mirror of
https://github.com/sassanix/Warracker.git
synced 2026-01-06 05:29:39 -06:00
Integrate Paperless-ngx for advanced document management and hybrid storage
Introduced full integration with Paperless-ngx to enable intelligent document management and flexible storage options. Key changes: - Added admin settings section for configuring Paperless-ngx (server URL, API token, connection testing, toggle). - Implemented hybrid storage logic in `backend/app.py` allowing per-document selection between local and Paperless-ngx. - Enhanced warranty card UI with visual indicators for storage location (cloud vs. local icons). - Integrated storage selection and upload process into both Add and Edit Warranty workflows with parity. - Enabled direct access to Paperless-ngx documents via the warranty interface. - Ensured automatic cleanup of old documents when storage preference is switched. Affected files: - `backend/app.py` - `frontend/script.js` - `frontend/settings-new.html` - `frontend/settings-new.js`
This commit is contained in:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,4 +1,28 @@
|
||||
# Changelog
|
||||
## 0.10.1.3 - 2025-06-17
|
||||
|
||||
### Added
|
||||
- **Paperless-ngx Document Management Integration:** Complete integration with Paperless-ngx for advanced document management and storage capabilities.
|
||||
- **Settings Page Configuration:** Added Paperless-ngx configuration section in admin settings allowing administrators to:
|
||||
- **Connection Setup:** Configure Paperless-ngx server URL and API token for secure integration
|
||||
- **Test Connection:** Built-in connection testing to verify Paperless-ngx server accessibility and authentication
|
||||
- **Enable/Disable Toggle:** Master switch to activate or deactivate Paperless-ngx integration across the application
|
||||
- **Hybrid Storage System (`backend/app.py`):** Implemented intelligent document storage with per-document storage choice:
|
||||
- **Storage Selection:** Users can choose between local storage and Paperless-ngx for each document type (invoice, manual, photos, other documents)
|
||||
- **Smart File Handling:** Documents are stored exclusively in chosen location (prevents dual storage)
|
||||
- **Database Integration:** Paperless document IDs properly tracked in database fields (`paperless_invoice_id`, `paperless_manual_id`, etc.)
|
||||
- **Automatic Cleanup:** When switching storage methods, old files are automatically removed from previous location
|
||||
- **Visual Document Identification:** Enhanced warranty cards with clear visual indicators for document storage location:
|
||||
- **Cloud Icons:** Blue cloud icons (🌤️) appear next to documents stored in Paperless-ngx for instant recognition
|
||||
- **Local Document Icons:** Standard document icons for locally stored files
|
||||
- **Mixed Storage Support:** Warranties can have documents stored in both locations with appropriate visual indicators
|
||||
- **Add/Edit Warranty Integration:** Full Paperless-ngx support in both add and edit warranty workflows:
|
||||
- **Storage Option Selection:** Radio buttons for each document type allowing users to choose storage location
|
||||
- **Seamless Upload Process:** Files uploaded to Paperless-ngx are automatically tagged and organized
|
||||
- **Edit Modal Parity:** Edit warranty modal has identical Paperless-ngx functionality to add warranty modal
|
||||
- **Document Access Integration:** Direct access to Paperless-ngx documents through warranty interface with secure authentication
|
||||
- _Files: `backend/app.py`, `frontend/script.js`, `frontend/settings-new.html`, `frontend/settings-new.js`_
|
||||
|
||||
|
||||
## 0.10.1.2 - 2025-06-16
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ COPY backend/extensions.py /app/backend/
|
||||
COPY backend/oidc_handler.py /app/backend/
|
||||
COPY backend/apprise_handler.py /app/backend/
|
||||
COPY backend/notifications.py /app/backend/
|
||||
COPY backend/paperless_handler.py /app/backend/
|
||||
|
||||
# Copy other utility scripts and migrations
|
||||
COPY backend/fix_permissions.py .
|
||||
|
||||
992
backend/app.py
992
backend/app.py
File diff suppressed because it is too large
Load Diff
@@ -1,134 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Diagnostic script to check Apprise installation and functionality
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def check_python_environment():
|
||||
"""Check Python environment details"""
|
||||
print("=== Python Environment ===")
|
||||
print(f"Python version: {sys.version}")
|
||||
print(f"Python executable: {sys.executable}")
|
||||
print(f"Python path: {sys.path}")
|
||||
print()
|
||||
|
||||
def check_apprise_import():
|
||||
"""Test Apprise import"""
|
||||
print("=== Apprise Import Test ===")
|
||||
try:
|
||||
import apprise
|
||||
print("✅ Apprise imported successfully")
|
||||
print(f" Apprise version: {getattr(apprise, '__version__', 'Unknown')}")
|
||||
print(f" Apprise location: {apprise.__file__}")
|
||||
return True
|
||||
except ImportError as e:
|
||||
print(f"❌ Failed to import Apprise: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error importing Apprise: {e}")
|
||||
return False
|
||||
|
||||
def check_apprise_functionality():
|
||||
"""Test basic Apprise functionality"""
|
||||
print("=== Apprise Functionality Test ===")
|
||||
try:
|
||||
import apprise
|
||||
|
||||
# Test creating an Apprise object
|
||||
apobj = apprise.Apprise()
|
||||
print("✅ Apprise object created successfully")
|
||||
|
||||
# Test adding a fake URL (won't send anything)
|
||||
fake_url = "json://httpbin.org/post"
|
||||
result = apobj.add(fake_url)
|
||||
if result:
|
||||
print("✅ Apprise URL addition test passed")
|
||||
else:
|
||||
print("⚠️ Apprise URL addition test failed (this may be normal)")
|
||||
|
||||
# Test notification (won't actually send)
|
||||
print("✅ Apprise basic functionality test completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Apprise functionality test failed: {e}")
|
||||
return False
|
||||
|
||||
def check_requirements_file():
|
||||
"""Check if apprise is in requirements.txt"""
|
||||
print("=== Requirements File Check ===")
|
||||
req_paths = [
|
||||
"/app/requirements.txt",
|
||||
"requirements.txt",
|
||||
"backend/requirements.txt"
|
||||
]
|
||||
|
||||
for req_path in req_paths:
|
||||
if os.path.exists(req_path):
|
||||
print(f"Found requirements file: {req_path}")
|
||||
try:
|
||||
with open(req_path, 'r') as f:
|
||||
content = f.read()
|
||||
if 'apprise' in content.lower():
|
||||
print("✅ Apprise found in requirements.txt")
|
||||
# Show the specific line
|
||||
for line in content.split('\n'):
|
||||
if 'apprise' in line.lower():
|
||||
print(f" {line.strip()}")
|
||||
else:
|
||||
print("❌ Apprise NOT found in requirements.txt")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error reading {req_path}: {e}")
|
||||
else:
|
||||
print("❌ No requirements.txt file found")
|
||||
print()
|
||||
|
||||
def check_installed_packages():
|
||||
"""Check what packages are installed"""
|
||||
print("=== Installed Packages Check ===")
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run([sys.executable, "-m", "pip", "list"],
|
||||
capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
packages = result.stdout
|
||||
if 'apprise' in packages.lower():
|
||||
print("✅ Apprise found in installed packages")
|
||||
for line in packages.split('\n'):
|
||||
if 'apprise' in line.lower():
|
||||
print(f" {line.strip()}")
|
||||
else:
|
||||
print("❌ Apprise NOT found in installed packages")
|
||||
print("Available packages:")
|
||||
print(packages[:500] + "..." if len(packages) > 500 else packages)
|
||||
else:
|
||||
print(f"❌ Failed to list packages: {result.stderr}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error checking installed packages: {e}")
|
||||
print()
|
||||
|
||||
def main():
|
||||
"""Run all diagnostic checks"""
|
||||
print("Apprise Diagnostic Script")
|
||||
print("=" * 50)
|
||||
|
||||
check_python_environment()
|
||||
check_requirements_file()
|
||||
check_installed_packages()
|
||||
|
||||
apprise_imported = check_apprise_import()
|
||||
|
||||
if apprise_imported:
|
||||
check_apprise_functionality()
|
||||
print("\n=== Summary ===")
|
||||
print("✅ Apprise is working correctly!")
|
||||
else:
|
||||
print("\n=== Summary ===")
|
||||
print("❌ Apprise is not available or not working")
|
||||
print(" Try installing it with: pip install apprise==1.9.3")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,129 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check warranties and their expiration dates
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
# Add the backend directory to the path
|
||||
sys.path.insert(0, '/app')
|
||||
sys.path.insert(0, '/app/backend')
|
||||
|
||||
print("🔍 Checking Warranties and Expiration Dates")
|
||||
print("=" * 60)
|
||||
|
||||
# Test imports
|
||||
try:
|
||||
from backend.db_handler import get_db_connection, release_db_connection, init_db_pool, get_expiring_warranties
|
||||
print("✅ Database functions imported successfully")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to import database functions: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test database connection
|
||||
try:
|
||||
init_db_pool()
|
||||
print("✅ Database pool initialized")
|
||||
except Exception as e:
|
||||
print(f"❌ Database connection failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Check all warranties
|
||||
conn = None
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\n1. Checking all warranties...")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
id, product_name, expiration_date, is_lifetime,
|
||||
purchase_date, user_id
|
||||
FROM warranties
|
||||
ORDER BY expiration_date ASC
|
||||
""")
|
||||
|
||||
warranties = cursor.fetchall()
|
||||
print(f" Total warranties in database: {len(warranties)}")
|
||||
|
||||
if warranties:
|
||||
print("\n Sample warranties:")
|
||||
today = date.today()
|
||||
|
||||
for i, warranty in enumerate(warranties[:10]): # Show first 10
|
||||
warranty_id, product_name, exp_date, is_lifetime, purchase_date, user_id = warranty
|
||||
|
||||
if is_lifetime:
|
||||
days_until_exp = "∞ (Lifetime)"
|
||||
elif exp_date:
|
||||
days_until_exp = (exp_date - today).days
|
||||
if days_until_exp < 0:
|
||||
days_until_exp = f"{abs(days_until_exp)} days ago (EXPIRED)"
|
||||
else:
|
||||
days_until_exp = f"{days_until_exp} days"
|
||||
else:
|
||||
days_until_exp = "No expiration date"
|
||||
|
||||
print(f" {i+1}. {product_name[:30]:<30} | Expires: {exp_date} | {days_until_exp}")
|
||||
|
||||
# Check specifically for warranties expiring soon
|
||||
print("\n2. Checking warranties expiring in next 30 days...")
|
||||
|
||||
today = date.today()
|
||||
next_30_days = today + timedelta(days=30)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
id, product_name, expiration_date, user_id,
|
||||
(expiration_date - %s) as days_until_expiry
|
||||
FROM warranties
|
||||
WHERE is_lifetime = false
|
||||
AND expiration_date BETWEEN %s AND %s
|
||||
ORDER BY expiration_date ASC
|
||||
""", (today, today, next_30_days))
|
||||
|
||||
expiring_soon = cursor.fetchall()
|
||||
print(f" Warranties expiring in next 30 days: {len(expiring_soon)}")
|
||||
|
||||
if expiring_soon:
|
||||
print("\n Expiring warranties:")
|
||||
for warranty in expiring_soon:
|
||||
warranty_id, product_name, exp_date, user_id, days_until = warranty
|
||||
print(f" • {product_name} (ID: {warranty_id}, User: {user_id}) - Expires: {exp_date} ({days_until.days} days)")
|
||||
|
||||
# Test the get_expiring_warranties function specifically
|
||||
print("\n3. Testing get_expiring_warranties function...")
|
||||
|
||||
for days in [1, 7, 14, 30, 60, 90]:
|
||||
try:
|
||||
expiring = get_expiring_warranties(days)
|
||||
print(f" Expiring in {days} days: {len(expiring)} warranties")
|
||||
|
||||
if expiring and days <= 30: # Show details for shorter timeframes
|
||||
for w in expiring[:3]: # Show first 3
|
||||
print(f" - {w.get('product_name', 'Unknown')} (expires: {w.get('expiration_date', 'Unknown')})")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error checking {days} days: {e}")
|
||||
|
||||
cursor.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error checking warranties: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎯 Summary:")
|
||||
print("\nIf you see 0 warranties expiring soon, you have a few options:")
|
||||
print("1. Add a test warranty with an upcoming expiration date")
|
||||
print("2. Modify an existing warranty to expire soon")
|
||||
print("3. Test with longer notification periods (like 60, 90 days)")
|
||||
print("\nTo add a test warranty:")
|
||||
print("- Go to your Warracker dashboard")
|
||||
print("- Add a new warranty with expiration date in the next few days")
|
||||
print("- Then test the Apprise notifications again")
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create a test warranty for testing Apprise notifications
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
# Add the backend directory to the path
|
||||
sys.path.insert(0, '/app')
|
||||
sys.path.insert(0, '/app/backend')
|
||||
|
||||
print("🧪 Creating Test Warranty for Apprise Notifications")
|
||||
print("=" * 60)
|
||||
|
||||
# Test imports
|
||||
try:
|
||||
from backend.db_handler import get_db_connection, release_db_connection, init_db_pool
|
||||
print("✅ Database functions imported successfully")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to import database functions: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test database connection
|
||||
try:
|
||||
init_db_pool()
|
||||
print("✅ Database pool initialized")
|
||||
except Exception as e:
|
||||
print(f"❌ Database connection failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Get the first user ID to assign the test warranty to
|
||||
conn = None
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get a user ID (preferably admin)
|
||||
cursor.execute("SELECT id FROM users WHERE is_admin = true LIMIT 1")
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
cursor.execute("SELECT id FROM users LIMIT 1")
|
||||
any_user = cursor.fetchone()
|
||||
if any_user:
|
||||
user_id = any_user[0]
|
||||
print(f"📋 Using first available user ID: {user_id}")
|
||||
else:
|
||||
print("❌ No users found in database")
|
||||
sys.exit(1)
|
||||
else:
|
||||
user_id = admin_user[0]
|
||||
print(f"📋 Using admin user ID: {user_id}")
|
||||
|
||||
# Create test warranties with different expiration dates
|
||||
today = date.today()
|
||||
test_warranties = [
|
||||
{
|
||||
'product_name': 'Test Product - Expires in 5 days',
|
||||
'expiration_date': today + timedelta(days=5),
|
||||
'purchase_date': today - timedelta(days=360),
|
||||
'vendor': 'Test Vendor',
|
||||
'warranty_type': 'Extended',
|
||||
'notes': 'Test warranty for Apprise notification testing'
|
||||
},
|
||||
{
|
||||
'product_name': 'Test Product - Expires in 15 days',
|
||||
'expiration_date': today + timedelta(days=15),
|
||||
'purchase_date': today - timedelta(days=350),
|
||||
'vendor': 'Test Vendor',
|
||||
'warranty_type': 'Standard',
|
||||
'notes': 'Test warranty for Apprise notification testing'
|
||||
},
|
||||
{
|
||||
'product_name': 'Test Product - Expires in 25 days',
|
||||
'expiration_date': today + timedelta(days=25),
|
||||
'purchase_date': today - timedelta(days=340),
|
||||
'vendor': 'Test Vendor',
|
||||
'warranty_type': 'Manufacturer',
|
||||
'notes': 'Test warranty for Apprise notification testing'
|
||||
}
|
||||
]
|
||||
|
||||
print(f"\n📝 Creating {len(test_warranties)} test warranties...")
|
||||
|
||||
created_warranties = []
|
||||
|
||||
for warranty in test_warranties:
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO warranties (
|
||||
product_name, purchase_date, expiration_date,
|
||||
user_id, vendor, warranty_type, notes,
|
||||
is_lifetime, warranty_duration_years, warranty_duration_months, warranty_duration_days
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
warranty['product_name'],
|
||||
warranty['purchase_date'],
|
||||
warranty['expiration_date'],
|
||||
user_id,
|
||||
warranty['vendor'],
|
||||
warranty['warranty_type'],
|
||||
warranty['notes'],
|
||||
False, # not lifetime
|
||||
1, # warranty_duration_years
|
||||
0, # warranty_duration_months
|
||||
0 # warranty_duration_days
|
||||
))
|
||||
|
||||
warranty_id = cursor.fetchone()[0]
|
||||
created_warranties.append({
|
||||
'id': warranty_id,
|
||||
'name': warranty['product_name'],
|
||||
'expires': warranty['expiration_date']
|
||||
})
|
||||
|
||||
print(f" ✅ Created: {warranty['product_name']} (ID: {warranty_id}, expires: {warranty['expiration_date']})")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Failed to create {warranty['product_name']}: {e}")
|
||||
|
||||
# Commit the changes
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
print(f"\n🎉 Successfully created {len(created_warranties)} test warranties!")
|
||||
|
||||
if created_warranties:
|
||||
print("\n📅 Test warranty schedule:")
|
||||
for warranty in created_warranties:
|
||||
days_until = (warranty['expires'] - today).days
|
||||
print(f" • {warranty['name']} - {days_until} days from now ({warranty['expires']})")
|
||||
|
||||
print("\n🧪 To test notifications:")
|
||||
print("1. Go to your Warracker admin settings")
|
||||
print("2. Click 'Send Expiration Notifications' in the Apprise section")
|
||||
print("3. Check your Discord/notification service for messages")
|
||||
print("\n🧹 To clean up test data later:")
|
||||
print("- Go to your warranty dashboard and delete the test warranties")
|
||||
print("- Or run a cleanup script")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating test warranties: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if conn:
|
||||
conn.rollback()
|
||||
finally:
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎯 Test warranties created! Now you can test Apprise notifications.")
|
||||
@@ -1,200 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script for Apprise settings saving and loading
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the path
|
||||
sys.path.insert(0, '/app')
|
||||
sys.path.insert(0, '/app/backend')
|
||||
|
||||
print("🔍 Debugging Apprise Settings")
|
||||
print("=" * 50)
|
||||
|
||||
# Test imports
|
||||
print("\n1. Testing imports...")
|
||||
try:
|
||||
from backend.db_handler import get_db_connection, release_db_connection, init_db_pool, get_site_setting, update_site_setting
|
||||
print("✅ Database functions imported successfully")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to import database functions: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test database connection
|
||||
print("\n2. Testing database connection...")
|
||||
try:
|
||||
init_db_pool()
|
||||
print("✅ Database pool initialized")
|
||||
except Exception as e:
|
||||
print(f"❌ Database connection failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if site_settings table exists
|
||||
print("\n3. Checking site_settings table...")
|
||||
conn = None
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if table exists
|
||||
cursor.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'site_settings'
|
||||
)
|
||||
""")
|
||||
table_exists = cursor.fetchone()[0]
|
||||
print(f"✅ site_settings table exists: {table_exists}")
|
||||
|
||||
if table_exists:
|
||||
# Check table structure
|
||||
cursor.execute("""
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'site_settings'
|
||||
ORDER BY ordinal_position
|
||||
""")
|
||||
columns = cursor.fetchall()
|
||||
print(" Table structure:")
|
||||
for column in columns:
|
||||
print(f" {column[0]} ({column[1]}) {'NULL' if column[2] == 'YES' else 'NOT NULL'}")
|
||||
|
||||
# Check existing Apprise settings
|
||||
cursor.execute("SELECT key, value FROM site_settings WHERE key LIKE 'apprise_%'")
|
||||
apprise_settings = cursor.fetchall()
|
||||
print(f"\n Existing Apprise settings ({len(apprise_settings)}):")
|
||||
for key, value in apprise_settings:
|
||||
print(f" {key} = {value}")
|
||||
|
||||
cursor.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error checking table: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
|
||||
# Test setting and getting values
|
||||
print("\n4. Testing setting and getting values...")
|
||||
test_key = "apprise_test_debug"
|
||||
test_value = "test_value_123"
|
||||
|
||||
try:
|
||||
# Test setting a value
|
||||
print(f" Setting {test_key} = {test_value}")
|
||||
success = update_site_setting(test_key, test_value)
|
||||
print(f" Update result: {success}")
|
||||
|
||||
if success:
|
||||
# Test getting the value
|
||||
retrieved_value = get_site_setting(test_key, "default")
|
||||
print(f" Retrieved value: {retrieved_value}")
|
||||
|
||||
if retrieved_value == test_value:
|
||||
print("✅ Setting and getting values works correctly")
|
||||
else:
|
||||
print(f"❌ Value mismatch! Expected: {test_value}, Got: {retrieved_value}")
|
||||
else:
|
||||
print("❌ Failed to update setting")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing set/get: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Test specific Apprise settings
|
||||
print("\n5. Testing Apprise-specific settings...")
|
||||
apprise_test_settings = {
|
||||
'apprise_enabled': 'true',
|
||||
'apprise_urls': 'test://url1,test://url2',
|
||||
'apprise_expiration_days': '7,30',
|
||||
'apprise_notification_time': '09:00',
|
||||
'apprise_title_prefix': '[Test Warracker]'
|
||||
}
|
||||
|
||||
try:
|
||||
for key, value in apprise_test_settings.items():
|
||||
print(f" Testing {key} = {value}")
|
||||
|
||||
# Save the setting
|
||||
success = update_site_setting(key, value)
|
||||
if not success:
|
||||
print(f" ❌ Failed to save {key}")
|
||||
continue
|
||||
|
||||
# Retrieve the setting
|
||||
retrieved = get_site_setting(key, "NOT_FOUND")
|
||||
if retrieved == value:
|
||||
print(f" ✅ {key} saved and retrieved correctly")
|
||||
else:
|
||||
print(f" ❌ {key} mismatch! Expected: {value}, Got: {retrieved}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing Apprise settings: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Test database query directly for troubleshooting
|
||||
print("\n6. Direct database query test...")
|
||||
conn = None
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Insert test setting directly
|
||||
test_direct_key = "apprise_direct_test"
|
||||
test_direct_value = "direct_test_value"
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO site_settings (key, value, updated_at)
|
||||
VALUES (%s, %s, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP
|
||||
""", (test_direct_key, test_direct_value))
|
||||
|
||||
conn.commit()
|
||||
print(f" ✅ Direct insert of {test_direct_key} successful")
|
||||
|
||||
# Query it back
|
||||
cursor.execute("SELECT value FROM site_settings WHERE key = %s", (test_direct_key,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result and result[0] == test_direct_value:
|
||||
print(f" ✅ Direct query retrieved correct value: {result[0]}")
|
||||
else:
|
||||
print(f" ❌ Direct query failed or wrong value: {result}")
|
||||
|
||||
cursor.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error with direct database test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
|
||||
# Check if API route works by simulating request
|
||||
print("\n7. Testing API route simulation...")
|
||||
try:
|
||||
# Import Flask components
|
||||
from backend.app import app
|
||||
|
||||
with app.test_client() as client:
|
||||
# This won't work without proper authentication, but we can check if the route exists
|
||||
print(" ✅ App imported successfully - API routes should be available")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error importing app: {e}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("🎯 Debug completed!")
|
||||
print("\nIf settings are saving but not appearing in frontend:")
|
||||
print("1. Check browser network tab for API errors")
|
||||
print("2. Check if frontend is calling the correct API endpoints")
|
||||
print("3. Verify authentication token is valid")
|
||||
print("4. Check browser console for JavaScript errors")
|
||||
14
backend/migrations/036_add_paperless_ngx_columns.sql
Normal file
14
backend/migrations/036_add_paperless_ngx_columns.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Migration: Add Paperless-ngx document ID columns to warranties table
|
||||
-- This allows storing Paperless-ngx document IDs as an alternative to local file storage
|
||||
|
||||
-- Add columns for storing Paperless-ngx document IDs
|
||||
ALTER TABLE warranties ADD COLUMN IF NOT EXISTS paperless_invoice_id INTEGER DEFAULT NULL;
|
||||
ALTER TABLE warranties ADD COLUMN IF NOT EXISTS paperless_manual_id INTEGER DEFAULT NULL;
|
||||
ALTER TABLE warranties ADD COLUMN IF NOT EXISTS paperless_photo_id INTEGER DEFAULT NULL;
|
||||
ALTER TABLE warranties ADD COLUMN IF NOT EXISTS paperless_other_id INTEGER DEFAULT NULL;
|
||||
|
||||
-- Add indexes for better performance when filtering by Paperless-ngx document existence
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_paperless_invoice ON warranties(paperless_invoice_id) WHERE paperless_invoice_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_paperless_manual ON warranties(paperless_manual_id) WHERE paperless_manual_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_paperless_photo ON warranties(paperless_photo_id) WHERE paperless_photo_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_paperless_other ON warranties(paperless_other_id) WHERE paperless_other_id IS NOT NULL;
|
||||
468
backend/paperless_handler.py
Normal file
468
backend/paperless_handler.py
Normal file
@@ -0,0 +1,468 @@
|
||||
"""
|
||||
Paperless-ngx API Handler for Warracker
|
||||
|
||||
This module provides functionality to interact with Paperless-ngx API
|
||||
for uploading, retrieving, and managing documents.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaperlessHandler:
|
||||
"""Handle interactions with Paperless-ngx API"""
|
||||
|
||||
def __init__(self, paperless_url: str, api_token: str):
|
||||
"""
|
||||
Initialize Paperless handler
|
||||
|
||||
Args:
|
||||
paperless_url: Base URL of Paperless-ngx instance (e.g., https://paperless.example.com)
|
||||
api_token: API token for authentication
|
||||
"""
|
||||
self.paperless_url = paperless_url.rstrip('/')
|
||||
self.api_token = api_token
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'Authorization': f'Token {api_token}',
|
||||
'User-Agent': 'Warracker-PaperlessIntegration/1.0'
|
||||
})
|
||||
|
||||
def test_connection(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
Test connection to Paperless-ngx instance
|
||||
|
||||
Returns:
|
||||
(success: bool, message: str)
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(f'{self.paperless_url}/api/documents/', params={'page_size': 1})
|
||||
response.raise_for_status()
|
||||
return True, "Connection successful"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "Cannot connect to Paperless-ngx instance. Check URL and network connectivity."
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "Connection timeout. Paperless-ngx instance might be slow or unresponsive."
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
return False, "Authentication failed. Check your API token."
|
||||
elif e.response.status_code == 403:
|
||||
return False, "Access forbidden. Check your API token permissions."
|
||||
else:
|
||||
return False, f"HTTP error: {e.response.status_code} - {e.response.reason}"
|
||||
except Exception as e:
|
||||
return False, f"Unexpected error: {str(e)}"
|
||||
|
||||
def upload_document(self, file_content: bytes, filename: str, title: Optional[str] = None,
|
||||
tags: Optional[list] = None, correspondent: Optional[str] = None) -> Tuple[bool, Optional[int], str]:
|
||||
"""
|
||||
Upload a document to Paperless-ngx
|
||||
|
||||
Args:
|
||||
file_content: Binary content of the file
|
||||
filename: Original filename
|
||||
title: Optional title for the document
|
||||
tags: Optional list of tag names
|
||||
correspondent: Optional correspondent name
|
||||
|
||||
Returns:
|
||||
(success: bool, document_id: Optional[int], message: str)
|
||||
"""
|
||||
try:
|
||||
# Detect MIME type from filename
|
||||
import mimetypes
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
if not mime_type:
|
||||
mime_type = 'application/octet-stream'
|
||||
|
||||
# Prepare files for multipart upload
|
||||
# Paperless-ngx expects the file under 'document' field
|
||||
files = {
|
||||
'document': (filename, BytesIO(file_content), mime_type)
|
||||
}
|
||||
|
||||
# Prepare form data - Paperless-ngx API requirements
|
||||
# Note: Don't include 'document' in data, only in files
|
||||
data = {}
|
||||
if title:
|
||||
data['title'] = title
|
||||
|
||||
# TODO: For future enhancement, implement proper tag/correspondent handling:
|
||||
# - correspondent expects PK (ID) of existing correspondent, not string name
|
||||
# - tags expects PKs (IDs) of existing tags, not string names
|
||||
# For now, we'll skip these optional fields to get basic upload working
|
||||
|
||||
# if correspondent:
|
||||
# # Would need to lookup/create correspondent ID first
|
||||
# data['correspondent'] = correspondent_id
|
||||
# if tags:
|
||||
# # Would need to lookup/create tag IDs first
|
||||
# data['tags'] = [tag_id1, tag_id2, ...]
|
||||
|
||||
logger.info(f"Uploading document to Paperless-ngx: {filename}")
|
||||
logger.info(f"Upload data: {data}")
|
||||
logger.info(f"MIME type: {mime_type}")
|
||||
|
||||
# Don't set Content-Type manually - let requests handle it
|
||||
response = self.session.post(
|
||||
f'{self.paperless_url}/api/documents/post_document/',
|
||||
files=files,
|
||||
headers={'Authorization': f'Token {self.api_token}'},
|
||||
timeout=60 # Longer timeout for uploads
|
||||
)
|
||||
|
||||
logger.info(f"Paperless-ngx upload response status: {response.status_code}")
|
||||
logger.info(f"Paperless-ngx upload response text: {response.text[:500]}...") # First 500 chars
|
||||
|
||||
response.raise_for_status() # This will raise an exception for 4xx/5xx status codes
|
||||
|
||||
# Try to parse response as JSON first
|
||||
try:
|
||||
result = response.json()
|
||||
logger.info(f"Paperless-ngx upload response: {result}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not parse response as JSON: {e}")
|
||||
# If we can't parse JSON but got 200, treat as success
|
||||
return True, None, "Document uploaded successfully"
|
||||
|
||||
# Handle different possible response formats from Paperless-ngx
|
||||
document_id = None
|
||||
if isinstance(result, dict):
|
||||
# JSON object response
|
||||
if 'task_id' in result:
|
||||
# Task-based response (asynchronous processing)
|
||||
document_id = result.get('task_id')
|
||||
logger.info(f"Document upload task created: {document_id}")
|
||||
return True, document_id, "Document uploaded successfully (processing)"
|
||||
elif 'id' in result:
|
||||
# Direct document ID response (synchronous processing)
|
||||
document_id = result.get('id')
|
||||
logger.info(f"Document uploaded with ID: {document_id}")
|
||||
return True, document_id, "Document uploaded successfully"
|
||||
elif result.get('success') or response.status_code == 200:
|
||||
# Generic success response
|
||||
logger.info(f"Document uploaded successfully (generic success)")
|
||||
return True, None, "Document uploaded successfully"
|
||||
else:
|
||||
logger.warning(f"Unexpected JSON response format from Paperless-ngx: {result}")
|
||||
# Even if format is unexpected, if we got HTTP 200, it's likely successful
|
||||
return True, None, "Document uploaded successfully (unknown JSON format)"
|
||||
elif isinstance(result, str):
|
||||
# String response - might contain an ID or just be a success message
|
||||
logger.info(f"Document uploaded successfully (string response): {result}")
|
||||
# Try to extract an ID from the string if it looks like one
|
||||
import re
|
||||
id_match = re.search(r'\d+', result)
|
||||
if id_match:
|
||||
document_id = int(id_match.group())
|
||||
logger.info(f"Extracted document ID from string: {document_id}")
|
||||
return True, document_id, f"Document uploaded successfully: {result}"
|
||||
else:
|
||||
return True, None, f"Document uploaded successfully: {result}"
|
||||
else:
|
||||
# Other response type
|
||||
logger.warning(f"Unexpected response type from Paperless-ngx: {type(result)} - {result}")
|
||||
return True, None, "Document uploaded successfully (unknown response type)"
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return False, None, "Upload timeout. The file might be too large or the connection is slow."
|
||||
except requests.exceptions.HTTPError as e:
|
||||
error_msg = f"Upload failed: HTTP {e.response.status_code}"
|
||||
try:
|
||||
error_detail = e.response.json()
|
||||
logger.error(f"Paperless-ngx detailed error: {error_detail}")
|
||||
|
||||
if 'detail' in error_detail:
|
||||
error_msg += f" - {error_detail['detail']}"
|
||||
elif isinstance(error_detail, dict):
|
||||
# Handle field-specific errors
|
||||
error_parts = []
|
||||
for field, errors in error_detail.items():
|
||||
if isinstance(errors, list):
|
||||
error_parts.append(f"{field}: {', '.join(errors)}")
|
||||
else:
|
||||
error_parts.append(f"{field}: {errors}")
|
||||
if error_parts:
|
||||
error_msg += f" - {'; '.join(error_parts)}"
|
||||
else:
|
||||
error_msg += f" - {error_detail}"
|
||||
except Exception as parse_error:
|
||||
logger.error(f"Could not parse error response: {parse_error}")
|
||||
error_msg += f" - {e.response.reason}"
|
||||
return False, None, error_msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading document to Paperless-ngx: {e}")
|
||||
return False, None, f"Upload failed: {str(e)}"
|
||||
|
||||
def get_document_preview(self, document_id: int) -> Tuple[bool, Optional[bytes], str, Optional[str]]:
|
||||
"""
|
||||
Get document preview/content from Paperless-ngx
|
||||
|
||||
Args:
|
||||
document_id: Paperless-ngx document ID
|
||||
|
||||
Returns:
|
||||
(success: bool, content: Optional[bytes], message: str, content_type: Optional[str])
|
||||
"""
|
||||
# Try multiple endpoints in order of preference
|
||||
endpoints_to_try = [
|
||||
('preview', f'/api/documents/{document_id}/preview/'),
|
||||
('download', f'/api/documents/{document_id}/download/'),
|
||||
]
|
||||
|
||||
last_error = None
|
||||
|
||||
for endpoint_name, endpoint_path in endpoints_to_try:
|
||||
try:
|
||||
logger.info(f"Fetching document {endpoint_name} from Paperless-ngx: {document_id}")
|
||||
response = self.session.get(
|
||||
f'{self.paperless_url}{endpoint_path}',
|
||||
timeout=30
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
content_type = response.headers.get('Content-Type', 'application/octet-stream')
|
||||
logger.info(f"Successfully retrieved document {document_id} via {endpoint_name} endpoint")
|
||||
return True, response.content, f"Document retrieved successfully via {endpoint_name}", content_type
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.warning(f"Failed to retrieve document {document_id} via {endpoint_name}: HTTP {e.response.status_code}")
|
||||
last_error = e
|
||||
if e.response.status_code == 404:
|
||||
continue # Try next endpoint
|
||||
else:
|
||||
# For non-404 errors, don't try other endpoints
|
||||
return False, None, f"Failed to retrieve document: HTTP {e.response.status_code}", None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error retrieving document {document_id} via {endpoint_name}: {e}")
|
||||
last_error = e
|
||||
continue # Try next endpoint
|
||||
|
||||
# If we get here, all endpoints failed
|
||||
if last_error and isinstance(last_error, requests.exceptions.HTTPError) and last_error.response.status_code == 404:
|
||||
return False, None, "Document not found in Paperless-ngx", None
|
||||
else:
|
||||
return False, None, f"Retrieval failed: {str(last_error) if last_error else 'All endpoints failed'}", None
|
||||
|
||||
def get_document_thumbnail(self, document_id: int) -> Tuple[bool, Optional[bytes], str]:
|
||||
"""
|
||||
Get document thumbnail from Paperless-ngx
|
||||
|
||||
Args:
|
||||
document_id: Paperless-ngx document ID
|
||||
|
||||
Returns:
|
||||
(success: bool, content: Optional[bytes], message: str)
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f'{self.paperless_url}/api/documents/{document_id}/thumb/',
|
||||
timeout=15
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
return True, response.content, "Thumbnail retrieved successfully"
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
return False, None, "Document or thumbnail not found"
|
||||
else:
|
||||
return False, None, f"Failed to retrieve thumbnail: HTTP {e.response.status_code}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving thumbnail from Paperless-ngx: {e}")
|
||||
return False, None, f"Thumbnail retrieval failed: {str(e)}"
|
||||
|
||||
def search_documents(self, query: str, limit: int = 25) -> Tuple[bool, Optional[list], str]:
|
||||
"""
|
||||
Search documents in Paperless-ngx
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
(success: bool, documents: Optional[list], message: str)
|
||||
"""
|
||||
try:
|
||||
params = {
|
||||
'query': query,
|
||||
'page_size': min(limit, 100) # Cap at 100 for performance
|
||||
}
|
||||
|
||||
response = self.session.get(
|
||||
f'{self.paperless_url}/api/documents/',
|
||||
params=params,
|
||||
timeout=15
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
documents = result.get('results', [])
|
||||
return True, documents, f"Found {len(documents)} documents"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching documents in Paperless-ngx: {e}")
|
||||
return False, None, f"Search failed: {str(e)}"
|
||||
|
||||
def get_document_info(self, document_id: int) -> Tuple[bool, Optional[Dict[str, Any]], str]:
|
||||
"""
|
||||
Get document information from Paperless-ngx
|
||||
|
||||
Args:
|
||||
document_id: Paperless-ngx document ID
|
||||
|
||||
Returns:
|
||||
(success: bool, document_info: Optional[Dict], message: str)
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f'{self.paperless_url}/api/documents/{document_id}/',
|
||||
timeout=15
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
document_info = response.json()
|
||||
|
||||
return True, document_info, "Document info retrieved successfully"
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
return False, None, "Document not found"
|
||||
else:
|
||||
return False, None, f"Failed to retrieve document info: HTTP {e.response.status_code}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving document info from Paperless-ngx: {e}")
|
||||
return False, None, f"Info retrieval failed: {str(e)}"
|
||||
|
||||
def debug_document_status(self, document_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Debug method to check document status and available endpoints
|
||||
|
||||
Args:
|
||||
document_id: Paperless-ngx document ID
|
||||
|
||||
Returns:
|
||||
Dictionary with debug information
|
||||
"""
|
||||
debug_info = {
|
||||
'document_id': document_id,
|
||||
'endpoints_tested': {},
|
||||
'document_exists': False,
|
||||
'document_info': None
|
||||
}
|
||||
|
||||
# Test different endpoints
|
||||
endpoints_to_test = [
|
||||
('info', f'/api/documents/{document_id}/'),
|
||||
('preview', f'/api/documents/{document_id}/preview/'),
|
||||
('download', f'/api/documents/{document_id}/download/'),
|
||||
('thumb', f'/api/documents/{document_id}/thumb/')
|
||||
]
|
||||
|
||||
for endpoint_name, endpoint_path in endpoints_to_test:
|
||||
try:
|
||||
logger.info(f"Testing endpoint: {self.paperless_url}{endpoint_path}")
|
||||
response = self.session.get(f'{self.paperless_url}{endpoint_path}', timeout=15)
|
||||
|
||||
debug_info['endpoints_tested'][endpoint_name] = {
|
||||
'status_code': response.status_code,
|
||||
'success': response.status_code < 400,
|
||||
'content_type': response.headers.get('Content-Type', 'unknown'),
|
||||
'content_length': len(response.content) if response.content else 0
|
||||
}
|
||||
|
||||
if endpoint_name == 'info' and response.status_code == 200:
|
||||
debug_info['document_exists'] = True
|
||||
try:
|
||||
debug_info['document_info'] = response.json()
|
||||
except:
|
||||
debug_info['document_info'] = 'Could not parse JSON'
|
||||
|
||||
except Exception as e:
|
||||
debug_info['endpoints_tested'][endpoint_name] = {
|
||||
'error': str(e),
|
||||
'success': False
|
||||
}
|
||||
|
||||
# Also try to list recent documents to see if our document is there
|
||||
try:
|
||||
response = self.session.get(f'{self.paperless_url}/api/documents/',
|
||||
params={'ordering': '-created', 'page_size': 10},
|
||||
timeout=15)
|
||||
if response.status_code == 200:
|
||||
recent_docs = response.json().get('results', [])
|
||||
debug_info['recent_documents'] = [
|
||||
{'id': doc.get('id'), 'title': doc.get('title'), 'created': doc.get('created')}
|
||||
for doc in recent_docs
|
||||
]
|
||||
debug_info['document_in_recent'] = any(doc.get('id') == document_id for doc in recent_docs)
|
||||
else:
|
||||
debug_info['recent_documents'] = f'Error: {response.status_code}'
|
||||
except Exception as e:
|
||||
debug_info['recent_documents'] = f'Exception: {str(e)}'
|
||||
|
||||
return debug_info
|
||||
|
||||
def document_exists(self, document_id: int) -> bool:
|
||||
"""
|
||||
Check if a document exists in Paperless-ngx
|
||||
|
||||
Args:
|
||||
document_id: Paperless-ngx document ID
|
||||
|
||||
Returns:
|
||||
True if document exists, False otherwise
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f'{self.paperless_url}/api/documents/{document_id}/',
|
||||
timeout=10
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking document existence {document_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_paperless_handler(conn) -> Optional[PaperlessHandler]:
|
||||
"""
|
||||
Get a configured Paperless handler from site settings
|
||||
|
||||
Args:
|
||||
conn: Database connection
|
||||
|
||||
Returns:
|
||||
PaperlessHandler instance or None if not configured/enabled
|
||||
"""
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT key, value FROM site_settings
|
||||
WHERE key IN ('paperless_enabled', 'paperless_url', 'paperless_api_token')
|
||||
""")
|
||||
settings = {row[0]: row[1] for row in cur.fetchall()}
|
||||
|
||||
# Check if Paperless-ngx is enabled
|
||||
if settings.get('paperless_enabled', 'false').lower() != 'true':
|
||||
return None
|
||||
|
||||
# Check required settings
|
||||
paperless_url = settings.get('paperless_url', '').strip()
|
||||
paperless_token = settings.get('paperless_api_token', '').strip()
|
||||
|
||||
if not paperless_url or not paperless_token:
|
||||
logger.warning("Paperless-ngx is enabled but URL or API token is missing")
|
||||
return None
|
||||
|
||||
return PaperlessHandler(paperless_url, paperless_token)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Paperless handler: {e}")
|
||||
return None
|
||||
@@ -312,7 +312,7 @@
|
||||
<!-- Hero Section -->
|
||||
<div class="about-hero">
|
||||
<h1><i class="fas fa-shield-alt"></i> Warracker</h1>
|
||||
<div class="version">Version v0.10.1.2</div>
|
||||
<div class="version">Version v0.10.1.3</div>
|
||||
<p class="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>
|
||||
|
||||
@@ -442,8 +442,31 @@
|
||||
</div>
|
||||
<!-- Documents Tab -->
|
||||
<div class="tab-content" id="documents">
|
||||
<!-- Paperless-ngx Info Alert (hidden by default, shown when enabled) -->
|
||||
<div id="paperlessInfoAlert" class="alert alert-info" style="display: none;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Paperless-ngx Integration Enabled!</strong> You can now choose to store documents in your Paperless-ngx instance instead of locally.
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Product Photo (Optional)</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="productPhotoStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="productPhotoStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="productPhotoStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="productPhoto" class="file-input-label">
|
||||
<i class="fas fa-camera"></i> Choose Photo
|
||||
@@ -458,6 +481,23 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Invoice/Receipt</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="invoiceStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="invoiceStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="invoiceStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="invoice" class="file-input-label">
|
||||
<i class="fas fa-upload"></i> Choose File
|
||||
@@ -469,6 +509,23 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Product Manual (Optional)</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="manualStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="manualStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="manualStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="manual" class="file-input-label">
|
||||
<i class="fas fa-upload"></i> Choose File
|
||||
@@ -479,6 +536,23 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Files (ZIP/RAR, Optional)</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="otherDocumentStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="otherDocumentStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="otherDocumentStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="otherDocument" class="file-input-label">
|
||||
<i class="fas fa-upload"></i> Choose File
|
||||
@@ -760,6 +834,23 @@
|
||||
<div class="edit-tab-content" id="edit-documents">
|
||||
<div class="form-group">
|
||||
<label>Product Photo (Optional)</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="editProductPhotoStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editProductPhotoStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editProductPhotoStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="editProductPhoto" class="file-input-label">
|
||||
<i class="fas fa-camera"></i> Choose Photo
|
||||
@@ -776,6 +867,23 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Invoice/Receipt</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="editInvoiceStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editInvoiceStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editInvoiceStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="editInvoice" class="file-input-label">
|
||||
<i class="fas fa-upload"></i> Choose File
|
||||
@@ -789,6 +897,23 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Product Manual (Optional)</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="editManualStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editManualStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editManualStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="editManual" class="file-input-label">
|
||||
<i class="fas fa-upload"></i> Choose File
|
||||
@@ -801,6 +926,23 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Files (ZIP/RAR, Optional)</label>
|
||||
<!-- Storage Selection -->
|
||||
<div id="editOtherDocumentStorageSelection" class="storage-selection" style="display: none;">
|
||||
<div class="storage-options">
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editOtherDocumentStorage" value="local" checked>
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-hdd"></i> Store Locally
|
||||
</span>
|
||||
</label>
|
||||
<label class="storage-option">
|
||||
<input type="radio" name="editOtherDocumentStorage" value="paperless">
|
||||
<span class="storage-label">
|
||||
<i class="fas fa-cloud"></i> Store in Paperless-ngx
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="editOtherDocument" class="file-input-label">
|
||||
<i class="fas fa-upload"></i> Choose File
|
||||
@@ -900,7 +1042,7 @@
|
||||
</div>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="script.js?v=20250529005"></script>
|
||||
<script src="script.js?v=20250617007"></script>
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="footer-fix.js"></script>
|
||||
|
||||
@@ -2238,18 +2238,9 @@ async function renderWarranties(warrantiesToRender) {
|
||||
<i class="fas fa-globe"></i> Product Website
|
||||
</a>
|
||||
` : ''}
|
||||
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
|
||||
<i class="fas fa-file-invoice"></i> Invoice
|
||||
</a>` : ''}
|
||||
${warranty.manual_path && warranty.manual_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
|
||||
<i class="fas fa-book"></i> Manual
|
||||
</a>` : ''}
|
||||
${warranty.other_document_path && warranty.other_document_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.other_document_path}'); return false;" class="other-document-link">
|
||||
<i class="fas fa-file-alt"></i> Files
|
||||
</a>` : ''}
|
||||
${generateDocumentLink(warranty, 'invoice')}
|
||||
${generateDocumentLink(warranty, 'manual')}
|
||||
${generateDocumentLink(warranty, 'other')}
|
||||
${notesLinkHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2305,18 +2296,9 @@ async function renderWarranties(warrantiesToRender) {
|
||||
<i class="fas fa-globe"></i> Product Website
|
||||
</a>
|
||||
` : ''}
|
||||
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
|
||||
<i class="fas fa-file-invoice"></i> Invoice
|
||||
</a>` : ''}
|
||||
${warranty.manual_path && warranty.manual_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
|
||||
<i class="fas fa-book"></i> Manual
|
||||
</a>` : ''}
|
||||
${warranty.other_document_path && warranty.other_document_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.other_document_path}'); return false;" class="other-document-link">
|
||||
<i class="fas fa-file-alt"></i> Files
|
||||
</a>` : ''}
|
||||
${generateDocumentLink(warranty, 'invoice')}
|
||||
${generateDocumentLink(warranty, 'manual')}
|
||||
${generateDocumentLink(warranty, 'other')}
|
||||
${notesLinkHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2361,18 +2343,9 @@ async function renderWarranties(warrantiesToRender) {
|
||||
<i class="fas fa-globe"></i> Product Website
|
||||
</a>
|
||||
` : ''}
|
||||
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
|
||||
<i class="fas fa-file-invoice"></i> Invoice
|
||||
</a>` : ''}
|
||||
${warranty.manual_path && warranty.manual_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
|
||||
<i class="fas fa-book"></i> Manual
|
||||
</a>` : ''}
|
||||
${warranty.other_document_path && warranty.other_document_path !== 'null' ? `
|
||||
<a href="#" onclick="openSecureFile('${warranty.other_document_path}'); return false;" class="other-document-link">
|
||||
<i class="fas fa-file-alt"></i> Files
|
||||
</a>` : ''}
|
||||
${generateDocumentLink(warranty, 'invoice')}
|
||||
${generateDocumentLink(warranty, 'manual')}
|
||||
${generateDocumentLink(warranty, 'other')}
|
||||
${notesLinkHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2670,7 +2643,10 @@ async function openEditModal(warranty) {
|
||||
const currentInvoiceElement = document.getElementById('currentInvoice');
|
||||
const deleteInvoiceBtn = document.getElementById('deleteInvoiceBtn');
|
||||
if (currentInvoiceElement && deleteInvoiceBtn) {
|
||||
if (warranty.invoice_path && warranty.invoice_path !== 'null') {
|
||||
const hasLocalInvoice = warranty.invoice_path && warranty.invoice_path !== 'null';
|
||||
const hasPaperlessInvoice = warranty.paperless_invoice_id && warranty.paperless_invoice_id !== null;
|
||||
|
||||
if (hasLocalInvoice) {
|
||||
currentInvoiceElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current invoice:
|
||||
@@ -2679,6 +2655,15 @@ async function openEditModal(warranty) {
|
||||
</span>
|
||||
`;
|
||||
deleteInvoiceBtn.style.display = '';
|
||||
} else if (hasPaperlessInvoice) {
|
||||
currentInvoiceElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current invoice:
|
||||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_invoice_id}); return false;">View</a>
|
||||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i> (Upload a new file to replace)
|
||||
</span>
|
||||
`;
|
||||
deleteInvoiceBtn.style.display = '';
|
||||
} else {
|
||||
currentInvoiceElement.innerHTML = '<span>No invoice uploaded</span>';
|
||||
deleteInvoiceBtn.style.display = 'none';
|
||||
@@ -2695,7 +2680,10 @@ async function openEditModal(warranty) {
|
||||
const currentManualElement = document.getElementById('currentManual');
|
||||
const deleteManualBtn = document.getElementById('deleteManualBtn');
|
||||
if (currentManualElement && deleteManualBtn) {
|
||||
if (warranty.manual_path && warranty.manual_path !== 'null') {
|
||||
const hasLocalManual = warranty.manual_path && warranty.manual_path !== 'null';
|
||||
const hasPaperlessManual = warranty.paperless_manual_id && warranty.paperless_manual_id !== null;
|
||||
|
||||
if (hasLocalManual) {
|
||||
currentManualElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current manual:
|
||||
@@ -2704,6 +2692,15 @@ async function openEditModal(warranty) {
|
||||
</span>
|
||||
`;
|
||||
deleteManualBtn.style.display = '';
|
||||
} else if (hasPaperlessManual) {
|
||||
currentManualElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current manual:
|
||||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_manual_id}); return false;">View</a>
|
||||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i> (Upload a new file to replace)
|
||||
</span>
|
||||
`;
|
||||
deleteManualBtn.style.display = '';
|
||||
} else {
|
||||
currentManualElement.innerHTML = '<span>No manual uploaded</span>';
|
||||
deleteManualBtn.style.display = 'none';
|
||||
@@ -2721,7 +2718,10 @@ async function openEditModal(warranty) {
|
||||
const currentProductPhotoElement = document.getElementById('currentProductPhoto');
|
||||
const deleteProductPhotoBtn = document.getElementById('deleteProductPhotoBtn');
|
||||
if (currentProductPhotoElement && deleteProductPhotoBtn) {
|
||||
if (warranty.product_photo_path && warranty.product_photo_path !== 'null') {
|
||||
const hasLocalPhoto = warranty.product_photo_path && warranty.product_photo_path !== 'null';
|
||||
const hasPaperlessPhoto = warranty.paperless_photo_id && warranty.paperless_photo_id !== null;
|
||||
|
||||
if (hasLocalPhoto) {
|
||||
currentProductPhotoElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current photo:
|
||||
@@ -2732,6 +2732,16 @@ async function openEditModal(warranty) {
|
||||
</span>
|
||||
`;
|
||||
deleteProductPhotoBtn.style.display = '';
|
||||
} else if (hasPaperlessPhoto) {
|
||||
currentProductPhotoElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current photo:
|
||||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_photo_id}); return false;">View</a>
|
||||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i>
|
||||
<br><small>(Upload a new photo to replace)</small>
|
||||
</span>
|
||||
`;
|
||||
deleteProductPhotoBtn.style.display = '';
|
||||
} else {
|
||||
currentProductPhotoElement.innerHTML = '<span>No photo uploaded</span>';
|
||||
deleteProductPhotoBtn.style.display = 'none';
|
||||
@@ -2749,7 +2759,10 @@ async function openEditModal(warranty) {
|
||||
const currentOtherDocumentElement = document.getElementById('currentOtherDocument');
|
||||
const deleteOtherDocumentBtn = document.getElementById('deleteOtherDocumentBtn');
|
||||
if (currentOtherDocumentElement && deleteOtherDocumentBtn) {
|
||||
if (warranty.other_document_path && warranty.other_document_path !== 'null') {
|
||||
const hasLocalOther = warranty.other_document_path && warranty.other_document_path !== 'null';
|
||||
const hasPaperlessOther = warranty.paperless_other_id && warranty.paperless_other_id !== null;
|
||||
|
||||
if (hasLocalOther) {
|
||||
currentOtherDocumentElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current other document:
|
||||
@@ -2758,6 +2771,15 @@ async function openEditModal(warranty) {
|
||||
</span>
|
||||
`;
|
||||
deleteOtherDocumentBtn.style.display = '';
|
||||
} else if (hasPaperlessOther) {
|
||||
currentOtherDocumentElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current other document:
|
||||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_other_id}); return false;">View</a>
|
||||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i> (Upload a new file to replace)
|
||||
</span>
|
||||
`;
|
||||
deleteOtherDocumentBtn.style.display = '';
|
||||
} else {
|
||||
currentOtherDocumentElement.innerHTML = '<span>No other document uploaded</span>';
|
||||
deleteOtherDocumentBtn.style.display = 'none';
|
||||
@@ -3192,50 +3214,59 @@ function handleFormSubmit(event) { // Renamed from submitForm
|
||||
// Show loading spinner
|
||||
showLoadingSpinner();
|
||||
|
||||
// Send the form data to the server
|
||||
fetch('/api/warranties', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error || 'Failed to add warranty');
|
||||
// Process Paperless-ngx uploads if enabled
|
||||
processPaperlessNgxUploads(formData)
|
||||
.then(paperlessUploads => {
|
||||
// Add Paperless-ngx document IDs to form data
|
||||
Object.keys(paperlessUploads).forEach(key => {
|
||||
formData.append(key, paperlessUploads[key]);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
hideLoadingSpinner();
|
||||
showToast('Warranty added successfully', 'success');
|
||||
|
||||
// --- Close and reset the modal on success ---
|
||||
if (addWarrantyModal) {
|
||||
addWarrantyModal.classList.remove('active');
|
||||
}
|
||||
resetAddWarrantyWizard(); // Reset the wizard form
|
||||
// --- End modification ---
|
||||
|
||||
// Send the form data to the server
|
||||
return fetch('/api/warranties', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error || 'Failed to add warranty');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
hideLoadingSpinner();
|
||||
showToast('Warranty added successfully', 'success');
|
||||
|
||||
// --- Close and reset the modal on success ---
|
||||
if (addWarrantyModal) {
|
||||
addWarrantyModal.classList.remove('active');
|
||||
}
|
||||
resetAddWarrantyWizard(); // Reset the wizard form
|
||||
// --- End modification ---
|
||||
|
||||
loadWarranties(true).then(() => {
|
||||
console.log('Warranties reloaded after adding new warranty');
|
||||
applyFilters();
|
||||
// Load secure images for the new cards - additional call to ensure they load
|
||||
setTimeout(() => {
|
||||
console.log('Loading secure images for new warranty cards');
|
||||
loadSecureImages();
|
||||
}, 200); // Slightly longer delay to ensure everything is rendered
|
||||
}).catch(error => {
|
||||
console.error('Error reloading warranties after adding:', error);
|
||||
}); // Reload the list and update UI
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoadingSpinner();
|
||||
console.error('Error adding warranty:', error);
|
||||
showToast(error.message || 'Failed to add warranty', 'error');
|
||||
});
|
||||
loadWarranties(true).then(() => {
|
||||
console.log('Warranties reloaded after adding new warranty');
|
||||
applyFilters();
|
||||
// Load secure images for the new cards - additional call to ensure they load
|
||||
setTimeout(() => {
|
||||
console.log('Loading secure images for new warranty cards');
|
||||
loadSecureImages();
|
||||
}, 200); // Slightly longer delay to ensure everything is rendered
|
||||
}).catch(error => {
|
||||
console.error('Error reloading warranties after adding:', error);
|
||||
}); // Reload the list and update UI
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoadingSpinner();
|
||||
console.error('Error adding warranty:', error);
|
||||
showToast(error.message || 'Failed to add warranty', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize page
|
||||
@@ -3387,7 +3418,7 @@ function openSecureFile(filePath) {
|
||||
showToast('Please log in to access files', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Enhanced fetch with retry logic and better error handling
|
||||
const fetchWithRetry = async (url, options, retries = 2) => {
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
@@ -3406,7 +3437,7 @@ function openSecureFile(filePath) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if response has content-length header
|
||||
const contentLength = response.headers.get('content-length');
|
||||
console.log(`[openSecureFile] Response Content-Length: ${contentLength}`);
|
||||
@@ -3499,6 +3530,65 @@ function openSecureFile(filePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a Paperless-ngx document by ID
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate document link HTML for both local and Paperless-ngx documents
|
||||
*/
|
||||
function generateDocumentLink(warranty, docType) {
|
||||
const docConfig = {
|
||||
invoice: {
|
||||
localPath: warranty.invoice_path,
|
||||
paperlessId: warranty.paperless_invoice_id,
|
||||
icon: 'fas fa-file-invoice',
|
||||
label: 'Invoice',
|
||||
className: 'invoice-link'
|
||||
},
|
||||
manual: {
|
||||
localPath: warranty.manual_path,
|
||||
paperlessId: warranty.paperless_manual_id,
|
||||
icon: 'fas fa-book',
|
||||
label: 'Manual',
|
||||
className: 'manual-link'
|
||||
},
|
||||
other: {
|
||||
localPath: warranty.other_document_path,
|
||||
paperlessId: warranty.paperless_other_id,
|
||||
icon: 'fas fa-file-alt',
|
||||
label: 'Files',
|
||||
className: 'other-document-link'
|
||||
},
|
||||
photo: {
|
||||
localPath: warranty.product_photo_path,
|
||||
paperlessId: warranty.paperless_photo_id,
|
||||
icon: 'fas fa-image',
|
||||
label: 'Photo',
|
||||
className: 'photo-link'
|
||||
}
|
||||
};
|
||||
|
||||
const config = docConfig[docType];
|
||||
if (!config) return '';
|
||||
|
||||
const hasLocal = config.localPath && config.localPath !== 'null';
|
||||
const hasPaperless = config.paperlessId && config.paperlessId !== null;
|
||||
|
||||
|
||||
if (hasLocal) {
|
||||
return `<a href="#" onclick="openSecureFile('${config.localPath}'); return false;" class="${config.className}">
|
||||
<i class="${config.icon}"></i> ${config.label}
|
||||
</a>`;
|
||||
} else if (hasPaperless) {
|
||||
return `<a href="#" onclick="openPaperlessDocument(${config.paperlessId}); return false;" class="${config.className}">
|
||||
<i class="${config.icon}"></i> ${config.label} <i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Initialize the warranty form and all its components
|
||||
function initWarrantyForm() {
|
||||
// Initialize form tabs
|
||||
@@ -4722,26 +4812,35 @@ function saveWarranty() {
|
||||
|
||||
showLoadingSpinner();
|
||||
|
||||
// Send request
|
||||
fetch(`/api/warranties/${currentWarrantyId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error || 'Failed to update warranty');
|
||||
// Process Paperless-ngx uploads if enabled
|
||||
processEditPaperlessNgxUploads(formData)
|
||||
.then(paperlessUploads => {
|
||||
// Add Paperless-ngx document IDs to form data
|
||||
Object.keys(paperlessUploads).forEach(key => {
|
||||
formData.append(key, paperlessUploads[key]);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
hideLoadingSpinner();
|
||||
showToast('Warranty updated successfully', 'success');
|
||||
closeModals();
|
||||
|
||||
// Send request
|
||||
return fetch(`/api/warranties/${currentWarrantyId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error || 'Failed to update warranty');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
hideLoadingSpinner();
|
||||
showToast('Warranty updated successfully', 'success');
|
||||
closeModals();
|
||||
|
||||
// Always reload from server to ensure we get the latest data including product photo paths
|
||||
console.log('Reloading warranties after edit to ensure latest data including product photos');
|
||||
@@ -6088,3 +6187,474 @@ async function loadSecureImages() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Paperless-ngx Integration Functions
|
||||
// ============================================================================
|
||||
|
||||
// Global variable to store Paperless-ngx enabled state
|
||||
let paperlessNgxEnabled = false;
|
||||
|
||||
/**
|
||||
* Check if Paperless-ngx integration is enabled
|
||||
*/
|
||||
async function checkPaperlessNgxStatus() {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) return false;
|
||||
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const settings = await response.json();
|
||||
paperlessNgxEnabled = settings.paperless_enabled === 'true';
|
||||
console.log('[Paperless-ngx] Integration status:', paperlessNgxEnabled);
|
||||
return paperlessNgxEnabled;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Paperless-ngx] Error checking status:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Paperless-ngx integration UI
|
||||
*/
|
||||
async function initPaperlessNgxIntegration() {
|
||||
// Check if Paperless-ngx is enabled
|
||||
const isEnabled = await checkPaperlessNgxStatus();
|
||||
|
||||
if (isEnabled) {
|
||||
// Show the info alert
|
||||
const infoAlert = document.getElementById('paperlessInfoAlert');
|
||||
if (infoAlert) {
|
||||
infoAlert.style.display = 'block';
|
||||
}
|
||||
|
||||
// Show storage selection options for add modal
|
||||
const storageSelections = [
|
||||
'productPhotoStorageSelection',
|
||||
'invoiceStorageSelection',
|
||||
'manualStorageSelection',
|
||||
'otherDocumentStorageSelection'
|
||||
];
|
||||
|
||||
storageSelections.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Show storage selection options for edit modal
|
||||
const editStorageSelections = [
|
||||
'editProductPhotoStorageSelection',
|
||||
'editInvoiceStorageSelection',
|
||||
'editManualStorageSelection',
|
||||
'editOtherDocumentStorageSelection'
|
||||
];
|
||||
|
||||
editStorageSelections.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Paperless-ngx] UI elements initialized and shown');
|
||||
} else {
|
||||
console.log('[Paperless-ngx] Integration disabled, hiding UI elements');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected storage option for a document type
|
||||
* @param {string} documentType - The document type (productPhoto, invoice, manual, otherDocument)
|
||||
* @param {boolean} isEdit - Whether this is for the edit modal
|
||||
* @returns {string} - 'local' or 'paperless'
|
||||
*/
|
||||
function getStorageOption(documentType, isEdit = false) {
|
||||
const prefix = isEdit ? 'edit' : '';
|
||||
const capitalizedType = documentType.charAt(0).toUpperCase() + documentType.slice(1);
|
||||
const name = `${prefix}${capitalizedType}Storage`;
|
||||
const radio = document.querySelector(`input[name="${name}"]:checked`);
|
||||
return radio ? radio.value : 'local';
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to Paperless-ngx
|
||||
* @param {File} file - The file to upload
|
||||
* @param {string} documentType - The type of document for tagging
|
||||
* @returns {Promise<Object>} - Upload result with document ID
|
||||
*/
|
||||
async function uploadToPaperlessNgx(file, documentType) {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
throw new Error('Authentication token not available');
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('document_type', documentType);
|
||||
formData.append('title', `Warracker ${documentType} - ${file.name}`);
|
||||
|
||||
// Add tags for organization
|
||||
const tags = ['warracker', documentType];
|
||||
formData.append('tags', tags.join(','));
|
||||
|
||||
console.log('[Paperless-ngx] Upload FormData contents:');
|
||||
console.log(' - file:', file.name, '(' + file.size + ' bytes, ' + file.type + ')');
|
||||
console.log(' - document_type:', documentType);
|
||||
console.log(' - title:', `Warracker ${documentType} - ${file.name}`);
|
||||
console.log(' - tags:', tags.join(','));
|
||||
|
||||
const response = await fetch('/api/paperless/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Failed to upload to Paperless-ngx';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorData.message || errorMessage;
|
||||
console.error('[Paperless-ngx] Server error details:', errorData);
|
||||
} catch (parseError) {
|
||||
console.error('[Paperless-ngx] Could not parse error response:', parseError);
|
||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[Paperless-ngx] Upload successful:', result);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
document_id: result.document_id,
|
||||
message: result.message
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Paperless-ngx] Upload error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle warranty form submission with Paperless-ngx integration
|
||||
* This extends the existing saveWarranty function
|
||||
*/
|
||||
async function processPaperlessNgxUploads(formData) {
|
||||
if (!paperlessNgxEnabled) {
|
||||
return {}; // Return empty object if not enabled
|
||||
}
|
||||
|
||||
const uploads = {};
|
||||
const documentTypes = ['productPhoto', 'invoice', 'manual', 'otherDocument'];
|
||||
|
||||
for (const docType of documentTypes) {
|
||||
const storageOption = getStorageOption(docType);
|
||||
|
||||
if (storageOption === 'paperless') {
|
||||
const fileInput = document.getElementById(docType);
|
||||
const file = fileInput?.files[0];
|
||||
|
||||
if (file) {
|
||||
console.log(`[Paperless-ngx] Uploading ${docType} to Paperless-ngx`);
|
||||
|
||||
// Upload to Paperless-ngx
|
||||
const uploadResult = await uploadToPaperlessNgx(file, docType);
|
||||
|
||||
if (uploadResult.success) {
|
||||
// Map frontend document types to database column names
|
||||
const fieldMapping = {
|
||||
'productPhoto': 'paperless_photo_id',
|
||||
'invoice': 'paperless_invoice_id',
|
||||
'manual': 'paperless_manual_id',
|
||||
'otherDocument': 'paperless_other_id'
|
||||
};
|
||||
|
||||
const dbField = fieldMapping[docType];
|
||||
if (dbField) {
|
||||
uploads[dbField] = uploadResult.document_id;
|
||||
}
|
||||
|
||||
// Remove the file from FormData since it's stored in Paperless-ngx
|
||||
if (formData.has(docType)) {
|
||||
formData.delete(docType);
|
||||
}
|
||||
|
||||
console.log(`[Paperless-ngx] ${docType} uploaded successfully, ID: ${uploadResult.document_id}, stored as: ${dbField}`);
|
||||
} else {
|
||||
throw new Error(`Failed to upload ${docType} to Paperless-ngx: ${uploadResult.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uploads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle warranty edit form submission with Paperless-ngx integration
|
||||
* This extends the existing edit warranty functionality
|
||||
*/
|
||||
async function processEditPaperlessNgxUploads(formData) {
|
||||
if (!paperlessNgxEnabled) {
|
||||
return {}; // Return empty object if not enabled
|
||||
}
|
||||
|
||||
const uploads = {};
|
||||
const documentTypes = ['productPhoto', 'invoice', 'manual', 'otherDocument'];
|
||||
|
||||
for (const docType of documentTypes) {
|
||||
const storageOption = getStorageOption(docType, true); // true for edit modal
|
||||
|
||||
if (storageOption === 'paperless') {
|
||||
const fileInput = document.getElementById(`edit${docType.charAt(0).toUpperCase() + docType.slice(1)}`);
|
||||
const file = fileInput?.files[0];
|
||||
|
||||
if (file) {
|
||||
console.log(`[Paperless-ngx] Uploading ${docType} to Paperless-ngx (edit mode)`);
|
||||
|
||||
// Upload to Paperless-ngx
|
||||
const uploadResult = await uploadToPaperlessNgx(file, docType);
|
||||
|
||||
if (uploadResult.success) {
|
||||
// Map frontend document types to database column names
|
||||
const fieldMapping = {
|
||||
'productPhoto': 'paperless_photo_id',
|
||||
'invoice': 'paperless_invoice_id',
|
||||
'manual': 'paperless_manual_id',
|
||||
'otherDocument': 'paperless_other_id'
|
||||
};
|
||||
|
||||
const dbField = fieldMapping[docType];
|
||||
if (dbField) {
|
||||
uploads[dbField] = uploadResult.document_id;
|
||||
}
|
||||
|
||||
// Remove the file from FormData since it's stored in Paperless-ngx
|
||||
const editFieldName = `edit${docType.charAt(0).toUpperCase() + docType.slice(1)}`;
|
||||
if (formData.has(editFieldName)) {
|
||||
formData.delete(editFieldName);
|
||||
}
|
||||
|
||||
console.log(`[Paperless-ngx] ${docType} uploaded successfully (edit), ID: ${uploadResult.document_id}, stored as: ${dbField}`);
|
||||
} else {
|
||||
throw new Error(`Failed to upload ${docType} to Paperless-ngx: ${uploadResult.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uploads;
|
||||
}
|
||||
|
||||
// Initialize Paperless-ngx integration when the page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize after a short delay to ensure other components are loaded
|
||||
setTimeout(() => {
|
||||
initPaperlessNgxIntegration();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Open a Paperless-ngx document in a new tab
|
||||
*/
|
||||
async function openPaperlessDocument(paperlessId) {
|
||||
console.log(`[openPaperlessDocument] Opening Paperless document: ${paperlessId}`);
|
||||
|
||||
const token = auth.getToken();
|
||||
if (!token) {
|
||||
console.error('[openPaperlessDocument] No auth token available');
|
||||
showToast('Authentication required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Retry mechanism for document retrieval
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 2000; // 2 seconds
|
||||
|
||||
const fetchWithRetry = async (attempt = 1) => {
|
||||
try {
|
||||
console.log(`[openPaperlessDocument] Attempt ${attempt} to fetch: /api/paperless-file/${paperlessId}`);
|
||||
|
||||
const response = await fetch(`/api/paperless-file/${paperlessId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
throw new Error('Document not found in Paperless-ngx.');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
// Get the blob and create object URL
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Open in new tab
|
||||
const newTab = window.open(url, '_blank');
|
||||
if (!newTab) {
|
||||
showToast('Please allow popups to view documents', 'warning');
|
||||
}
|
||||
|
||||
// Clean up the object URL after a delay
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[openPaperlessDocument] Attempt ${attempt} failed:`, error);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`[openPaperlessDocument] Retrying in ${retryDelay}ms...`);
|
||||
setTimeout(() => fetchWithRetry(attempt + 1), retryDelay);
|
||||
} else {
|
||||
console.error(`[openPaperlessDocument] All ${maxRetries} attempts failed`);
|
||||
showToast(`Failed to open document: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await fetchWithRetry();
|
||||
} catch (error) {
|
||||
console.error('Error fetching Paperless document:', error);
|
||||
showToast(`Error opening document: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug function to test Paperless document status
|
||||
*/
|
||||
async function debugPaperlessDocument(paperlessId) {
|
||||
console.log(`[debugPaperlessDocument] Debugging Paperless document: ${paperlessId}`);
|
||||
|
||||
const token = auth.getToken();
|
||||
if (!token) {
|
||||
console.error('[debugPaperlessDocument] No auth token available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/paperless/debug-document/${paperlessId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`[debugPaperlessDocument] HTTP ${response.status}: ${errorText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const debugInfo = await response.json();
|
||||
console.log(`[debugPaperlessDocument] Debug info for document ${paperlessId}:`, debugInfo);
|
||||
|
||||
// Show debug info in a more readable format
|
||||
let debugMessage = `Debug info for Paperless document ${paperlessId}:\n\n`;
|
||||
debugMessage += `Document exists: ${debugInfo.document_exists}\n`;
|
||||
debugMessage += `Database references: ${debugInfo.database_references?.length || 0}\n\n`;
|
||||
|
||||
debugMessage += 'Endpoint test results:\n';
|
||||
for (const [endpoint, result] of Object.entries(debugInfo.endpoints_tested || {})) {
|
||||
debugMessage += `- ${endpoint}: ${result.success ? 'SUCCESS' : 'FAILED'} (${result.status_code || result.error})\n`;
|
||||
}
|
||||
|
||||
if (debugInfo.recent_documents && Array.isArray(debugInfo.recent_documents)) {
|
||||
debugMessage += `\nDocument in recent list: ${debugInfo.document_in_recent}\n`;
|
||||
debugMessage += `Recent documents: ${debugInfo.recent_documents.map(d => `${d.id}: ${d.title}`).join(', ')}\n`;
|
||||
}
|
||||
|
||||
alert(debugMessage);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error debugging Paperless document:', error);
|
||||
alert(`Debug failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up invalid Paperless-ngx document references
|
||||
*/
|
||||
async function cleanupInvalidPaperlessDocuments() {
|
||||
console.log('[cleanupInvalidPaperlessDocuments] Starting cleanup...');
|
||||
|
||||
const token = auth.getToken();
|
||||
if (!token) {
|
||||
console.error('[cleanupInvalidPaperlessDocuments] No auth token available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/paperless/cleanup-invalid', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`[cleanupInvalidPaperlessDocuments] HTTP ${response.status}: ${errorText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[cleanupInvalidPaperlessDocuments] Cleanup result:', result);
|
||||
|
||||
// Show result to user
|
||||
let message = result.message || 'Cleanup completed';
|
||||
if (result.details) {
|
||||
message += `\n\nDetails:\n`;
|
||||
message += `- Documents checked: ${result.details.checked}\n`;
|
||||
message += `- Invalid documents found: ${result.details.invalid_found}\n`;
|
||||
message += `- References cleaned up: ${result.details.cleaned_up}\n`;
|
||||
|
||||
if (result.details.errors && result.details.errors.length > 0) {
|
||||
message += `\nErrors:\n${result.details.errors.join('\n')}`;
|
||||
}
|
||||
}
|
||||
|
||||
alert(message);
|
||||
|
||||
// Reload warranties to reflect changes
|
||||
if (result.details && result.details.cleaned_up > 0) {
|
||||
console.log('[cleanupInvalidPaperlessDocuments] Reloading warranties after cleanup...');
|
||||
await loadWarranties(true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up Paperless documents:', error);
|
||||
alert(`Cleanup failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Make debug and cleanup functions available globally for console testing
|
||||
window.debugPaperlessDocument = debugPaperlessDocument;
|
||||
window.cleanupInvalidPaperlessDocuments = cleanupInvalidPaperlessDocuments;
|
||||
|
||||
@@ -599,6 +599,71 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paperless-ngx Integration Configuration Card -->
|
||||
<div class="card collapsible-card">
|
||||
<div class="card-header collapsible-header" data-target="paperlessSettingsContent">
|
||||
<h3>Paperless-ngx Integration</h3>
|
||||
<i class="fas fa-chevron-down collapse-icon"></i>
|
||||
</div>
|
||||
<div class="card-body collapsible-content" id="paperlessSettingsContent">
|
||||
<div class="alert alert-info" style="margin-bottom: 20px;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>About Paperless-ngx Integration:</strong>
|
||||
This optional feature allows you to store warranty documents in your self-hosted Paperless-ngx instance instead of locally.
|
||||
When enabled, users can choose where to store each document: locally or in Paperless-ngx.
|
||||
</div>
|
||||
|
||||
<form id="paperlessSettingsForm">
|
||||
<div class="form-group">
|
||||
<div class="preference-item">
|
||||
<div>
|
||||
<label>Enable Paperless-ngx Integration</label>
|
||||
<p class="text-muted">Allow users to store warranty documents in Paperless-ngx</p>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="paperlessEnabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="paperlessSettingsContainer" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="paperlessUrl">Paperless-ngx URL</label>
|
||||
<input type="url" id="paperlessUrl" class="form-control" placeholder="https://paperless.yourdomain.com">
|
||||
<small class="text-muted">The base URL of your Paperless-ngx instance (without trailing slash).</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="paperlessApiToken">API Token</label>
|
||||
<input type="password" id="paperlessApiToken" class="form-control" placeholder="Enter API token or leave blank to keep existing">
|
||||
<small class="text-muted">Generate an API token from your Paperless-ngx instance (Settings → API Tokens).</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label>Connection Testing</label>
|
||||
<div class="paperless-button-group">
|
||||
<button type="button" id="testPaperlessConnectionBtn" class="btn btn-secondary">
|
||||
<i class="fas fa-plug"></i> Test Connection
|
||||
</button>
|
||||
<button type="button" id="debugPaperlessBtn" class="btn btn-info">
|
||||
<i class="fas fa-bug"></i> Debug Config
|
||||
</button>
|
||||
<button type="button" id="testFileUploadBtn" class="btn btn-warning">
|
||||
<i class="fas fa-upload"></i> Test Upload
|
||||
</button>
|
||||
</div>
|
||||
<div id="paperlessConnectionStatus" class="paperless-status-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="savePaperlessSettingsBtn" class="btn btn-primary" style="margin-top: 15px;">
|
||||
<i class="fas fa-save"></i> Save Paperless-ngx Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apprise Notifications Configuration Card -->
|
||||
<div class="card collapsible-card">
|
||||
<div class="card-header collapsible-header" data-target="appriseNotificationsContent">
|
||||
|
||||
@@ -102,6 +102,15 @@ const currencyPositionSelect = document.getElementById('currencyPositionSelect')
|
||||
// Add dateFormatSelect near other DOM element declarations if not already there
|
||||
const dateFormatSelect = document.getElementById('dateFormat');
|
||||
|
||||
// Paperless-ngx Settings DOM Elements
|
||||
const paperlessEnabledToggle = document.getElementById('paperlessEnabled');
|
||||
const paperlessUrlInput = document.getElementById('paperlessUrl');
|
||||
const paperlessApiTokenInput = document.getElementById('paperlessApiToken');
|
||||
const paperlessSettingsContainer = document.getElementById('paperlessSettingsContainer');
|
||||
const testPaperlessConnectionBtn = document.getElementById('testPaperlessConnectionBtn');
|
||||
const savePaperlessSettingsBtn = document.getElementById('savePaperlessSettingsBtn');
|
||||
const paperlessConnectionStatus = document.getElementById('paperlessConnectionStatus');
|
||||
|
||||
// Global variable to store currencies data for currency code lookup
|
||||
let globalCurrenciesData = [];
|
||||
|
||||
@@ -1111,6 +1120,52 @@ function setupEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Paperless-ngx event listeners
|
||||
if (paperlessEnabledToggle) {
|
||||
paperlessEnabledToggle.addEventListener('change', function() {
|
||||
togglePaperlessSettings(this.checked);
|
||||
});
|
||||
}
|
||||
|
||||
if (testPaperlessConnectionBtn) {
|
||||
testPaperlessConnectionBtn.addEventListener('click', testPaperlessConnection);
|
||||
}
|
||||
|
||||
if (savePaperlessSettingsBtn) {
|
||||
savePaperlessSettingsBtn.addEventListener('click', savePaperlessSettings);
|
||||
}
|
||||
|
||||
// Add debug button event listener
|
||||
const debugPaperlessBtn = document.getElementById('debugPaperlessBtn');
|
||||
if (debugPaperlessBtn) {
|
||||
debugPaperlessBtn.addEventListener('click', debugPaperlessConfiguration);
|
||||
}
|
||||
|
||||
// Add test upload button event listener
|
||||
const testFileUploadBtn = document.getElementById('testFileUploadBtn');
|
||||
if (testFileUploadBtn) {
|
||||
testFileUploadBtn.addEventListener('click', testFileUpload);
|
||||
}
|
||||
|
||||
// Clear status message when inputs are changed
|
||||
if (paperlessUrlInput) {
|
||||
paperlessUrlInput.addEventListener('input', function() {
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.className = 'paperless-status-message';
|
||||
paperlessConnectionStatus.innerHTML = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (paperlessApiTokenInput) {
|
||||
paperlessApiTokenInput.addEventListener('input', function() {
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.className = 'paperless-status-message';
|
||||
paperlessConnectionStatus.innerHTML = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Event listeners setup complete');
|
||||
}
|
||||
|
||||
@@ -4428,9 +4483,387 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentUser = window.auth.getCurrentUser();
|
||||
if (currentUser && currentUser.is_admin) {
|
||||
loadAppriseSettings();
|
||||
loadPaperlessSettings(); // Also load Paperless-ngx settings for admin
|
||||
} else {
|
||||
console.log('User is not admin, skipping Apprise settings load in deferred initialization');
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Paperless-ngx Integration Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Toggle the visibility of Paperless-ngx settings based on enabled state
|
||||
* @param {boolean} enabled - Whether Paperless-ngx is enabled
|
||||
*/
|
||||
function togglePaperlessSettings(enabled) {
|
||||
if (paperlessSettingsContainer) {
|
||||
paperlessSettingsContainer.style.display = enabled ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Paperless-ngx settings from the server
|
||||
*/
|
||||
async function loadPaperlessSettings() {
|
||||
try {
|
||||
const token = window.auth ? window.auth.getToken() : localStorage.getItem('auth_token');
|
||||
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load settings');
|
||||
}
|
||||
|
||||
const settings = await response.json();
|
||||
|
||||
// Update Paperless-ngx settings UI
|
||||
if (paperlessEnabledToggle) {
|
||||
const isEnabled = settings.paperless_enabled === 'true';
|
||||
paperlessEnabledToggle.checked = isEnabled;
|
||||
togglePaperlessSettings(isEnabled);
|
||||
}
|
||||
|
||||
if (paperlessUrlInput && settings.paperless_url) {
|
||||
paperlessUrlInput.value = settings.paperless_url;
|
||||
}
|
||||
|
||||
// Update API token field placeholder to indicate if token is saved
|
||||
if (paperlessApiTokenInput) {
|
||||
if (settings.paperless_api_token_set === 'true') {
|
||||
paperlessApiTokenInput.placeholder = 'API token saved (hidden for security) - Leave blank to keep existing';
|
||||
} else {
|
||||
paperlessApiTokenInput.placeholder = 'Enter API token';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Paperless-ngx settings loaded successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading Paperless-ngx settings:', error);
|
||||
showToast(`Error loading Paperless-ngx settings: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Paperless-ngx settings to the server
|
||||
*/
|
||||
async function savePaperlessSettings() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const token = window.auth ? window.auth.getToken() : localStorage.getItem('auth_token');
|
||||
|
||||
// Validate URL format
|
||||
const url = paperlessUrlInput.value.trim();
|
||||
if (url && !isValidUrl(url)) {
|
||||
showToast('Please enter a valid URL (e.g., https://paperless.yourdomain.com)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare settings data
|
||||
const settingsData = {
|
||||
paperless_enabled: paperlessEnabledToggle.checked.toString(),
|
||||
paperless_url: url
|
||||
};
|
||||
|
||||
// Only include API token if it's provided (not empty)
|
||||
const apiToken = paperlessApiTokenInput.value.trim();
|
||||
if (apiToken) {
|
||||
settingsData.paperless_api_token = apiToken;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(settingsData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to save Paperless-ngx settings');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
showToast('Paperless-ngx settings saved successfully!', 'success');
|
||||
|
||||
// Clear the API token input for security
|
||||
if (paperlessApiTokenInput) {
|
||||
paperlessApiTokenInput.value = '';
|
||||
paperlessApiTokenInput.placeholder = 'API token saved (hidden for security) - Leave blank to keep existing';
|
||||
}
|
||||
|
||||
console.log('✅ Paperless-ngx settings saved successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error saving Paperless-ngx settings:', error);
|
||||
showToast(`Error saving Paperless-ngx settings: ${error.message}`, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to Paperless-ngx instance
|
||||
*/
|
||||
async function testPaperlessConnection() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
// Clear previous status
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.innerHTML = '';
|
||||
paperlessConnectionStatus.className = 'paperless-status-message';
|
||||
}
|
||||
|
||||
const token = window.auth ? window.auth.getToken() : localStorage.getItem('auth_token');
|
||||
|
||||
// Get current values from the form
|
||||
const url = paperlessUrlInput.value.trim();
|
||||
const apiToken = paperlessApiTokenInput.value.trim();
|
||||
|
||||
if (!url) {
|
||||
showToast('Please enter a Paperless-ngx URL first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if API token is provided in form, otherwise use stored settings
|
||||
if (!apiToken && paperlessApiTokenInput.placeholder.includes('saved')) {
|
||||
// API token is already saved, test with stored settings
|
||||
const response = await fetch('/api/paperless/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
if (result.success) {
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show success';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>✅ Connection Successful!</strong><br>
|
||||
${result.message}
|
||||
${result.server_info ? `<br><small>Server: ${result.server_info}</small>` : ''}
|
||||
`;
|
||||
showToast('Paperless-ngx connection test successful!', 'success');
|
||||
} else {
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show error';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>❌ Connection Failed!</strong><br>
|
||||
${result.message}
|
||||
`;
|
||||
showToast('Paperless-ngx connection test failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiToken) {
|
||||
showToast('Please enter an API token to test the connection', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/paperless/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
api_token: apiToken
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (paperlessConnectionStatus) {
|
||||
if (result.success) {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show success';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>✅ Connection Successful!</strong><br>
|
||||
${result.message}
|
||||
${result.server_info ? `<br><small>Server: ${result.server_info}</small>` : ''}
|
||||
`;
|
||||
showToast('Paperless-ngx connection test successful!', 'success');
|
||||
} else {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show error';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>❌ Connection Failed!</strong><br>
|
||||
${result.message}
|
||||
`;
|
||||
showToast('Paperless-ngx connection test failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error testing Paperless-ngx connection:', error);
|
||||
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show error';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>❌ Connection Error!</strong><br>
|
||||
${error.message}
|
||||
`;
|
||||
}
|
||||
|
||||
showToast(`Error testing connection: ${error.message}`, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug Paperless-ngx configuration
|
||||
*/
|
||||
async function debugPaperlessConfiguration() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const token = window.auth ? window.auth.getToken() : localStorage.getItem('auth_token');
|
||||
|
||||
const response = await fetch('/api/paperless/debug', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Display debug information
|
||||
console.log('🔍 Paperless-ngx Debug Information:', result);
|
||||
|
||||
let debugHtml = '<h5>📋 Paperless-ngx Debug Information</h5>';
|
||||
debugHtml += `<strong>Enabled:</strong> ${result.paperless_enabled}<br>`;
|
||||
debugHtml += `<strong>URL:</strong> ${result.paperless_url || 'Not set'}<br>`;
|
||||
debugHtml += `<strong>API Token Set:</strong> ${result.paperless_api_token_set}<br>`;
|
||||
debugHtml += `<strong>Handler Available:</strong> ${result.paperless_handler_available}<br>`;
|
||||
|
||||
if (result.test_connection_result) {
|
||||
debugHtml += `<strong>Connection Test:</strong> ${result.test_connection_result.success ? '✅ Success' : '❌ Failed'}<br>`;
|
||||
debugHtml += `<strong>Message:</strong> ${result.test_connection_result.message || result.test_connection_result.error}<br>`;
|
||||
}
|
||||
|
||||
if (result.paperless_handler_error) {
|
||||
debugHtml += `<strong>Handler Error:</strong> ${result.paperless_handler_error}<br>`;
|
||||
}
|
||||
|
||||
// Show in connection status area or create a temporary area
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show info';
|
||||
paperlessConnectionStatus.innerHTML = debugHtml;
|
||||
} else {
|
||||
// Create temporary debug display
|
||||
const debugArea = document.createElement('div');
|
||||
debugArea.innerHTML = debugHtml;
|
||||
document.body.appendChild(debugArea);
|
||||
setTimeout(() => debugArea.remove(), 10000); // Remove after 10 seconds
|
||||
}
|
||||
|
||||
showToast('Debug information logged to console', 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error running Paperless debug:', error);
|
||||
showToast(`Debug error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test basic file upload mechanism
|
||||
*/
|
||||
async function testFileUpload() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
// Create a simple test file
|
||||
const testContent = 'This is a test file for Paperless-ngx upload debugging';
|
||||
const testFile = new File([testContent], 'test.txt', { type: 'text/plain' });
|
||||
|
||||
const token = window.auth ? window.auth.getToken() : localStorage.getItem('auth_token');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', testFile);
|
||||
formData.append('document_type', 'test');
|
||||
|
||||
const response = await fetch('/api/paperless/test-upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
console.log('🧪 File Upload Test Result:', result);
|
||||
|
||||
if (result.success) {
|
||||
showToast('File upload test successful!', 'success');
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show success';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>✅ File Upload Test Successful!</strong><br>
|
||||
${result.message}<br>
|
||||
<small>File: ${result.file_info.filename} (${result.file_info.size} bytes)</small>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
showToast('File upload test failed', 'error');
|
||||
if (paperlessConnectionStatus) {
|
||||
paperlessConnectionStatus.classList.add('show');
|
||||
paperlessConnectionStatus.className = 'paperless-status-message show error';
|
||||
paperlessConnectionStatus.innerHTML = `
|
||||
<strong>❌ File Upload Test Failed!</strong><br>
|
||||
${result.error}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error testing file upload:', error);
|
||||
showToast(`File upload test error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a string is a valid URL
|
||||
* @param {string} string - The string to validate
|
||||
* @returns {boolean} - Whether the string is a valid URL
|
||||
*/
|
||||
function isValidUrl(string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,3 +1225,79 @@ textarea.form-control {
|
||||
}
|
||||
}
|
||||
|
||||
/* Paperless-ngx Integration Styles */
|
||||
.paperless-button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.paperless-button-group .btn {
|
||||
flex: 0 0 auto;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.paperless-status-message {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--light-gray);
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.paperless-status-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paperless-status-message.success {
|
||||
background-color: rgba(40, 167, 69, 0.1);
|
||||
color: #155724;
|
||||
border: 1px solid rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.paperless-status-message.error {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
color: #721c24;
|
||||
border: 1px solid rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.paperless-status-message.info {
|
||||
background-color: rgba(23, 162, 184, 0.1);
|
||||
color: #0c5460;
|
||||
border: 1px solid rgba(23, 162, 184, 0.3);
|
||||
}
|
||||
|
||||
.dark-mode .paperless-status-message {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.dark-mode .paperless-status-message.success {
|
||||
background-color: rgba(40, 167, 69, 0.2);
|
||||
color: #5cb85c;
|
||||
}
|
||||
|
||||
.dark-mode .paperless-status-message.error {
|
||||
background-color: rgba(220, 53, 69, 0.2);
|
||||
color: #d9534f;
|
||||
}
|
||||
|
||||
.dark-mode .paperless-status-message.info {
|
||||
background-color: rgba(23, 162, 184, 0.2);
|
||||
color: #5bc0de;
|
||||
}
|
||||
|
||||
/* Responsive design for paperless buttons */
|
||||
@media (max-width: 576px) {
|
||||
.paperless-button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.paperless-button-group .btn {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<script src="chart.js"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
<script src="fix-auth-buttons-loader.js"></script>
|
||||
<script src="script.js?v=20250529005" defer></script> <!-- Added script.js -->
|
||||
<script src="script.js?v=20250617007" defer></script> <!-- Added script.js -->
|
||||
<script src="status.js" defer></script> <!-- Status page specific functionality -->
|
||||
<style>
|
||||
.user-menu {
|
||||
|
||||
@@ -216,4 +216,112 @@
|
||||
|
||||
#tagManagementModal {
|
||||
z-index: 1050;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Paperless-ngx Integration Styles
|
||||
============================================================================ */
|
||||
|
||||
/* Storage Selection Styles */
|
||||
.storage-selection {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.storage-options {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.storage-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
background-color: var(--bg-main);
|
||||
}
|
||||
|
||||
.storage-option:hover {
|
||||
background-color: var(--bg-accent);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.storage-option input[type="radio"] {
|
||||
margin: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.storage-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.storage-label i {
|
||||
font-size: 14px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.storage-option input[type="radio"]:checked + .storage-label {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.storage-option input[type="radio"]:checked + .storage-label i {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Paperless-ngx info alert */
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #e7f3ff;
|
||||
border-color: #b8d4f0;
|
||||
color: #31708f;
|
||||
}
|
||||
|
||||
.alert-info i {
|
||||
color: #31708f;
|
||||
}
|
||||
|
||||
/* Dark mode support for Paperless-ngx elements */
|
||||
.dark-mode .storage-selection {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.dark-mode .storage-option {
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dark-mode .storage-option:hover {
|
||||
background-color: var(--bg-accent);
|
||||
}
|
||||
|
||||
.dark-mode .alert-info {
|
||||
background-color: rgba(49, 112, 143, 0.1);
|
||||
border-color: rgba(49, 112, 143, 0.3);
|
||||
color: #a8c8e1;
|
||||
}
|
||||
|
||||
.dark-mode .alert-info i {
|
||||
color: #a8c8e1;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Version checker for Warracker
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentVersion = '0.10.1.2'; // Current version of the application
|
||||
const currentVersion = '0.10.1.3'; // Current version of the application
|
||||
const updateStatus = document.getElementById('updateStatus');
|
||||
const updateLink = document.getElementById('updateLink');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user