User management and module improvements
- Added daily_mirror module to permissions system - Fixed user module management - updates now work correctly - Implemented dashboard module filtering based on user permissions - Fixed warehouse create_locations page (config parser and delete) - Implemented POST-Redirect-GET pattern to prevent duplicate entries - Added application license system with validation middleware - Cleaned up debug logging code - Improved user module selection with fetch API instead of form submit
This commit is contained in:
@@ -20,6 +20,55 @@ def create_app():
|
|||||||
# Add 'now' function to Jinja2 globals
|
# Add 'now' function to Jinja2 globals
|
||||||
app.jinja_env.globals['now'] = datetime.now
|
app.jinja_env.globals['now'] = datetime.now
|
||||||
|
|
||||||
|
# Add license check middleware
|
||||||
|
@app.before_request
|
||||||
|
def check_license_middleware():
|
||||||
|
from flask import session, request, redirect, url_for, flash, render_template
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Skip license check for static files, login page, and superadmin users
|
||||||
|
if request.endpoint and (
|
||||||
|
request.endpoint == 'static' or
|
||||||
|
request.endpoint == 'main.login' or
|
||||||
|
request.path.startswith('/static/')
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Skip if user is not logged in (will be redirected to login by other means)
|
||||||
|
if 'user' not in session:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Skip license check for superadmin
|
||||||
|
if session.get('role') == 'superadmin':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check license validity
|
||||||
|
license_path = os.path.join(app.instance_path, 'app_license.json')
|
||||||
|
|
||||||
|
if not os.path.exists(license_path):
|
||||||
|
session.clear()
|
||||||
|
flash('⚠️ Application License Missing - Please contact your superadmin to generate a license key.', 'danger')
|
||||||
|
return redirect(url_for('main.login'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(license_path, 'r') as f:
|
||||||
|
license_data = json.load(f)
|
||||||
|
|
||||||
|
valid_until = datetime.strptime(license_data['valid_until'], '%Y-%m-%d')
|
||||||
|
|
||||||
|
if datetime.utcnow().date() > valid_until.date():
|
||||||
|
session.clear()
|
||||||
|
flash(f'⚠️ Application License Expired on {license_data["valid_until"]} - Please contact your superadmin to renew the license.', 'danger')
|
||||||
|
return redirect(url_for('main.login'))
|
||||||
|
except Exception as e:
|
||||||
|
session.clear()
|
||||||
|
flash('⚠️ License Validation Error - Please contact your superadmin.', 'danger')
|
||||||
|
return redirect(url_for('main.login'))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
# Initialize automatic backup scheduler
|
# Initialize automatic backup scheduler
|
||||||
from app.backup_scheduler import init_backup_scheduler
|
from app.backup_scheduler import init_backup_scheduler
|
||||||
init_backup_scheduler(app)
|
init_backup_scheduler(app)
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ MODULES = {
|
|||||||
'scan_pages': ['move_orders'],
|
'scan_pages': ['move_orders'],
|
||||||
'management_pages': ['create_locations', 'warehouse_reports', 'inventory_management'],
|
'management_pages': ['create_locations', 'warehouse_reports', 'inventory_management'],
|
||||||
'worker_access': ['move_orders_only'] # Workers can move orders but not create locations
|
'worker_access': ['move_orders_only'] # Workers can move orders but not create locations
|
||||||
|
},
|
||||||
|
'daily_mirror': {
|
||||||
|
'name': 'Daily Mirror (BI & Reports)',
|
||||||
|
'scan_pages': [],
|
||||||
|
'management_pages': ['daily_mirror', 'build_database', 'view_production_data'],
|
||||||
|
'worker_access': [] # Workers typically don't need access to BI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,28 @@ from .access_control import (
|
|||||||
bp = Blueprint('main', __name__)
|
bp = Blueprint('main', __name__)
|
||||||
warehouse_bp = Blueprint('warehouse', __name__)
|
warehouse_bp = Blueprint('warehouse', __name__)
|
||||||
|
|
||||||
|
def check_app_license():
|
||||||
|
"""Check if the application license is valid."""
|
||||||
|
license_path = os.path.join(current_app.instance_path, 'app_license.json')
|
||||||
|
|
||||||
|
# If no license file exists, return invalid
|
||||||
|
if not os.path.exists(license_path):
|
||||||
|
return False, '⚠️ Application License Expired\n\nThis application requires a valid license to operate.\nPlease contact your superadmin to renew the license.'
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(license_path, 'r') as f:
|
||||||
|
license_data = json.load(f)
|
||||||
|
|
||||||
|
valid_until = datetime.strptime(license_data['valid_until'], '%Y-%m-%d')
|
||||||
|
|
||||||
|
# Check if license is still valid
|
||||||
|
if datetime.utcnow().date() > valid_until.date():
|
||||||
|
return False, f'⚠️ Application License Expired\n\nThe license expired on {license_data["valid_until"]}.\nPlease contact your superadmin to renew the license.'
|
||||||
|
|
||||||
|
return True, 'License valid'
|
||||||
|
except Exception as e:
|
||||||
|
return False, f'⚠️ License Validation Error\n\nCould not validate application license.\nPlease contact your superadmin.'
|
||||||
|
|
||||||
@bp.route('/main_scan')
|
@bp.route('/main_scan')
|
||||||
@requires_quality_module
|
@requires_quality_module
|
||||||
def main_scan():
|
def main_scan():
|
||||||
@@ -97,6 +119,15 @@ def login():
|
|||||||
user_modules = ['quality', 'warehouse', 'labels', 'daily_mirror']
|
user_modules = ['quality', 'warehouse', 'labels', 'daily_mirror']
|
||||||
|
|
||||||
session['modules'] = user_modules
|
session['modules'] = user_modules
|
||||||
|
|
||||||
|
# Check app license for non-superadmin users
|
||||||
|
if user['role'] != 'superadmin':
|
||||||
|
license_valid, license_message = check_app_license()
|
||||||
|
if not license_valid:
|
||||||
|
session.clear()
|
||||||
|
flash(license_message, 'danger')
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
print("Logged in as:", session.get('user'), session.get('role'), "modules:", user_modules)
|
print("Logged in as:", session.get('user'), session.get('role'), "modules:", user_modules)
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
else:
|
else:
|
||||||
@@ -191,7 +222,16 @@ def dashboard():
|
|||||||
print("Session user:", session.get('user'), session.get('role'))
|
print("Session user:", session.get('user'), session.get('role'))
|
||||||
if 'user' not in session:
|
if 'user' not in session:
|
||||||
return redirect(url_for('main.login'))
|
return redirect(url_for('main.login'))
|
||||||
return render_template('dashboard.html')
|
|
||||||
|
# Get user's modules and role
|
||||||
|
user_role = session.get('role')
|
||||||
|
user_modules = session.get('modules', [])
|
||||||
|
|
||||||
|
# Superadmin and admin see all modules
|
||||||
|
if user_role in ['superadmin', 'admin']:
|
||||||
|
user_modules = ['quality', 'warehouse', 'labels', 'daily_mirror']
|
||||||
|
|
||||||
|
return render_template('dashboard.html', user_modules=user_modules, user_role=user_role)
|
||||||
|
|
||||||
@bp.route('/settings')
|
@bp.route('/settings')
|
||||||
@admin_plus
|
@admin_plus
|
||||||
@@ -402,7 +442,7 @@ def quick_update_modules():
|
|||||||
# Get current user to validate role
|
# Get current user to validate role
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT username, role FROM users WHERE id=%s", (user_id,))
|
cursor.execute("SELECT username, role, modules FROM users WHERE id=%s", (user_id,))
|
||||||
user_row = cursor.fetchone()
|
user_row = cursor.fetchone()
|
||||||
|
|
||||||
if not user_row:
|
if not user_row:
|
||||||
@@ -410,11 +450,12 @@ def quick_update_modules():
|
|||||||
conn.close()
|
conn.close()
|
||||||
return redirect(url_for('main.user_management_simple'))
|
return redirect(url_for('main.user_management_simple'))
|
||||||
|
|
||||||
username, role = user_row
|
username, role, current_modules = user_row
|
||||||
|
|
||||||
# Validate modules for the role
|
# Validate modules for the role
|
||||||
from app.permissions_simple import validate_user_modules
|
from app.permissions_simple import validate_user_modules
|
||||||
is_valid, error_msg = validate_user_modules(role, modules)
|
is_valid, error_msg = validate_user_modules(role, modules)
|
||||||
|
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
flash(f'Invalid module assignment: {error_msg}')
|
flash(f'Invalid module assignment: {error_msg}')
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -425,18 +466,25 @@ def quick_update_modules():
|
|||||||
if modules and role in ['manager', 'worker']:
|
if modules and role in ['manager', 'worker']:
|
||||||
import json
|
import json
|
||||||
modules_json = json.dumps(modules)
|
modules_json = json.dumps(modules)
|
||||||
|
elif not modules and role in ['manager', 'worker']:
|
||||||
|
# Empty modules list for manager/worker
|
||||||
|
import json
|
||||||
|
modules_json = json.dumps([])
|
||||||
|
|
||||||
# Update modules only
|
# Update modules only
|
||||||
cursor.execute("UPDATE users SET modules=%s WHERE id=%s", (modules_json, user_id))
|
cursor.execute("UPDATE users SET modules=%s WHERE id=%s", (modules_json, user_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
flash(f'Modules updated successfully for user "{username}".')
|
flash(f'Modules updated successfully for user "{username}". New modules: {", ".join(modules) if modules else "None"}', 'success')
|
||||||
return redirect(url_for('main.user_management_simple'))
|
return redirect(url_for('main.user_management_simple'))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating modules: {e}")
|
print(f"ERROR updating modules: {e}")
|
||||||
flash('Error updating modules.')
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
flash(f'Error updating modules: {str(e)}', 'danger')
|
||||||
return redirect(url_for('main.user_management_simple'))
|
return redirect(url_for('main.user_management_simple'))
|
||||||
|
|
||||||
@bp.route('/reports')
|
@bp.route('/reports')
|
||||||
@@ -2398,7 +2446,64 @@ def download_extension():
|
|||||||
keys = []
|
keys = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
keys = []
|
keys = []
|
||||||
return render_template('download_extension.html', pairing_keys=keys)
|
|
||||||
|
# Load app license key
|
||||||
|
license_path = os.path.join(current_app.instance_path, 'app_license.json')
|
||||||
|
license_data = None
|
||||||
|
try:
|
||||||
|
if os.path.exists(license_path):
|
||||||
|
with open(license_path, 'r') as f:
|
||||||
|
license_data = json.load(f)
|
||||||
|
|
||||||
|
# Calculate days remaining
|
||||||
|
valid_until = datetime.strptime(license_data['valid_until'], '%Y-%m-%d')
|
||||||
|
days_remaining = (valid_until.date() - datetime.utcnow().date()).days
|
||||||
|
license_data['days_remaining'] = days_remaining
|
||||||
|
except Exception as e:
|
||||||
|
license_data = None
|
||||||
|
|
||||||
|
return render_template('download_extension.html', pairing_keys=keys, license_data=license_data)
|
||||||
|
|
||||||
|
@bp.route('/generate_app_license', methods=['POST'])
|
||||||
|
@superadmin_only
|
||||||
|
def generate_app_license():
|
||||||
|
"""Generate a license key for the application."""
|
||||||
|
validity_days = int(request.form.get('validity_days', 365)) # Default to 365 days
|
||||||
|
|
||||||
|
# Generate a secure license key
|
||||||
|
license_key = secrets.token_urlsafe(48)
|
||||||
|
valid_until = (datetime.utcnow() + timedelta(days=validity_days)).strftime('%Y-%m-%d')
|
||||||
|
created_at = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Create license data
|
||||||
|
license_data = {
|
||||||
|
'license_key': license_key,
|
||||||
|
'valid_until': valid_until,
|
||||||
|
'created_at': created_at,
|
||||||
|
'validity_days': validity_days
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save license key
|
||||||
|
license_path = os.path.join(current_app.instance_path, 'app_license.json')
|
||||||
|
with open(license_path, 'w') as f:
|
||||||
|
json.dump(license_data, f, indent=2)
|
||||||
|
|
||||||
|
flash(f'App license generated successfully (valid for {validity_days} days until {valid_until}).', 'success')
|
||||||
|
return redirect(url_for('main.download_extension'))
|
||||||
|
|
||||||
|
@bp.route('/revoke_app_license', methods=['POST'])
|
||||||
|
@superadmin_only
|
||||||
|
def revoke_app_license():
|
||||||
|
"""Revoke the application license."""
|
||||||
|
license_path = os.path.join(current_app.instance_path, 'app_license.json')
|
||||||
|
|
||||||
|
if os.path.exists(license_path):
|
||||||
|
os.remove(license_path)
|
||||||
|
flash('App license revoked successfully.', 'success')
|
||||||
|
else:
|
||||||
|
flash('No license key found to revoke.', 'warning')
|
||||||
|
|
||||||
|
return redirect(url_for('main.download_extension'))
|
||||||
|
|
||||||
@bp.route('/extension_files/<path:filename>')
|
@bp.route('/extension_files/<path:filename>')
|
||||||
def extension_files(filename):
|
def extension_files(filename):
|
||||||
|
|||||||
@@ -702,35 +702,6 @@ function confirmDelete() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle delete confirmation
|
|
||||||
function confirmDelete() {
|
|
||||||
const locationId = document.getElementById('delete-confirm-id').textContent;
|
|
||||||
|
|
||||||
// Send delete request
|
|
||||||
fetch('/delete_location', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ location_id: locationId })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(result => {
|
|
||||||
if (result.success) {
|
|
||||||
showNotification('✅ Location deleted successfully!', 'success');
|
|
||||||
closeDeleteModal();
|
|
||||||
// Reload page to show changes
|
|
||||||
setTimeout(() => window.location.reload(), 1000);
|
|
||||||
} else {
|
|
||||||
showNotification('❌ Error deleting location: ' + result.error, 'error');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
showNotification('❌ Error deleting location: ' + error.message, 'error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modals when clicking outside
|
// Close modals when clicking outside
|
||||||
window.addEventListener('click', function(event) {
|
window.addEventListener('click', function(event) {
|
||||||
const editModal = document.getElementById('edit-modal');
|
const editModal = document.getElementById('edit-modal');
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container">
|
||||||
<!-- Row of evenly distributed cards -->
|
<!-- Row of evenly distributed cards -->
|
||||||
|
{% if 'quality' in user_modules %}
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>Quality Module</h3>
|
<h3>Quality Module</h3>
|
||||||
<p>Final scanning module for production orders and quality reports access.</p>
|
<p>Final scanning module for production orders and quality reports access.</p>
|
||||||
@@ -20,26 +21,34 @@
|
|||||||
<a href="{{ url_for('main.reports') }}" class="btn">Access to Quality reports</a>
|
<a href="{{ url_for('main.reports') }}" class="btn">Access to Quality reports</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'warehouse' in user_modules %}
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>Access Warehouse Module</h3>
|
<h3>Access Warehouse Module</h3>
|
||||||
<p>Access warehouse module functionalities.</p>
|
<p>Access warehouse module functionalities.</p>
|
||||||
<a href="{{ url_for('main.warehouse') }}" class="btn" id="launch-warehouse">Open Warehouse</a>
|
<a href="{{ url_for('main.warehouse') }}" class="btn" id="launch-warehouse">Open Warehouse</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'labels' in user_modules %}
|
||||||
<!-- New Card: Access Labels Module -->
|
<!-- New Card: Access Labels Module -->
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>Access Labels Module</h3>
|
<h3>Access Labels Module</h3>
|
||||||
<p>Module for label management.</p>
|
<p>Module for label management.</p>
|
||||||
<a href="{{ url_for('main.etichete') }}" class="btn">Launch Labels Module</a>
|
<a href="{{ url_for('main.etichete') }}" class="btn">Launch Labels Module</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_role in ['superadmin', 'admin'] %}
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>Manage Settings</h3>
|
<h3>Manage Settings</h3>
|
||||||
<p>Access and manage application settings.</p>
|
<p>Access and manage application settings.</p>
|
||||||
<a href="{{ url_for('main.settings') }}" class="btn">Access Settings Page</a>
|
<a href="{{ url_for('main.settings') }}" class="btn">Access Settings Page</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'daily_mirror' in user_modules %}
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>📊 Daily Mirror</h3>
|
<h3>📊 Daily Mirror</h3>
|
||||||
<p>Business Intelligence and Production Reporting - Generate comprehensive daily reports including order quantities, production status, and delivery tracking.</p>
|
<p>Business Intelligence and Production Reporting - Generate comprehensive daily reports including order quantities, production status, and delivery tracking.</p>
|
||||||
@@ -50,6 +59,7 @@
|
|||||||
<strong>Tracks:</strong> Orders quantity • Production launched • Production finished • Orders delivered
|
<strong>Tracks:</strong> Orders quantity • Production launched • Production finished • Orders delivered
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -304,6 +304,81 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="qz-pairing-card" style="margin-top: 40px;">
|
||||||
|
<h2>🔐 Application License Management</h2>
|
||||||
|
|
||||||
|
{% if license_data %}
|
||||||
|
<div class="qz-result" style="{% if license_data.days_remaining <= 30 %}background: #fff3e0; border-left-color: #ff9800;{% elif license_data.days_remaining <= 7 %}background: #ffebee; border-left-color: #f44336;{% endif %}">
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<strong>🔑 License Key:</strong> <span>{{ license_data.license_key }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<strong>📅 Created:</strong> {{ license_data.created_at }}
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<strong>⏰ Valid Until:</strong> {{ license_data.valid_until }}
|
||||||
|
{% if license_data.days_remaining is defined %}
|
||||||
|
{% if license_data.days_remaining > 0 %}
|
||||||
|
<span style="margin-left: 8px; padding: 2px 8px; background: {% if license_data.days_remaining <= 7 %}#f44336{% elif license_data.days_remaining <= 30 %}#ff9800{% else %}#4caf50{% endif %}; color: white; border-radius: 4px; font-size: 0.85em;">
|
||||||
|
{{ license_data.days_remaining }} days remaining
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="margin-left: 8px; padding: 2px 8px; background: #f44336; color: white; border-radius: 4px; font-size: 0.85em;">
|
||||||
|
⚠️ EXPIRED
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<form method="POST" action="/revoke_app_license" style="display: inline-block;">
|
||||||
|
<button class="btn-delete" type="submit" onclick="return confirm('Are you sure you want to REVOKE the application license?\n\nThis will prevent all non-superadmin users from accessing the application!\n\nThis action cannot be undone.');">
|
||||||
|
🗑️ Revoke License
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="padding: 24px; background: #ffebee; border-left: 4px solid #f44336; border-radius: 6px; margin-bottom: 24px;">
|
||||||
|
<strong style="color: #c62828;">⚠️ No Active License</strong>
|
||||||
|
<p style="margin: 8px 0 0 0; color: #d32f2f;">
|
||||||
|
No application license has been generated. Non-superadmin users will not be able to access the application.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>🔑 Generate New License</h3>
|
||||||
|
<form id="license-form" method="POST" action="/generate_app_license" class="qz-form-group">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="license_validity_days">License Validity Period:</label>
|
||||||
|
<select id="license_validity_days" name="validity_days">
|
||||||
|
<option value="30">30 Days (1 Month)</option>
|
||||||
|
<option value="90">90 Days (3 Months)</option>
|
||||||
|
<option value="180">180 Days (6 Months)</option>
|
||||||
|
<option value="365" selected>365 Days (1 Year)</option>
|
||||||
|
<option value="730">730 Days (2 Years)</option>
|
||||||
|
<option value="1825">1825 Days (5 Years)</option>
|
||||||
|
<option value="3650">3650 Days (10 Years)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" onclick="return confirm('{% if license_data %}This will replace the existing license key.\n\n{% endif %}Are you sure you want to generate a new application license?');">
|
||||||
|
🔑 Generate License Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="margin-top: 24px; padding: 16px; background: var(--info-bg, #e3f2fd); border-left: 4px solid #2196f3; border-radius: 6px;">
|
||||||
|
<strong style="color: var(--info-text, #0d47a1);">ℹ️ License Information</strong>
|
||||||
|
<ul style="margin: 8px 0 0 0; color: var(--info-text, #1565c0); font-size: 0.9em;">
|
||||||
|
<li>The application license controls access for all non-superadmin users</li>
|
||||||
|
<li>Superadmin accounts can always access the application regardless of license status</li>
|
||||||
|
<li>When the license expires, non-superadmin users will see an expiration message</li>
|
||||||
|
<li>Generating a new license will replace any existing license</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function deletePairingKey(pairingKey, printerName) {
|
function deletePairingKey(pairingKey, printerName) {
|
||||||
if (confirm(`Are you sure you want to delete the pairing key for "${printerName}"?\n\nKey: ${pairingKey}\n\nThis action cannot be undone.`)) {
|
if (confirm(`Are you sure you want to delete the pairing key for "${printerName}"?\n\nKey: ${pairingKey}\n\nThis action cannot be undone.`)) {
|
||||||
|
|||||||
@@ -51,6 +51,22 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if session.role == 'superadmin' %}
|
||||||
|
<div class="card" style="margin-top: 32px;">
|
||||||
|
<h3>🖨️ Print Extension Management</h3>
|
||||||
|
<p><strong>QZ Tray Pairing Keys:</strong> Manage printer connection keys</p>
|
||||||
|
<p>Control access to direct printing functionality for label modules</p>
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<a href="{{ url_for('main.download_extension') }}" class="btn" style="background-color: #4caf50; color: white;">
|
||||||
|
🔑 Manage Pairing Keys
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<small style="display: block; margin-top: 10px; color: #666;">
|
||||||
|
Generate and manage pairing keys for QZ Tray printer integration
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if session.role in ['superadmin', 'admin'] %}
|
{% if session.role in ['superadmin', 'admin'] %}
|
||||||
<div class="card backup-card" style="margin-top: 32px;">
|
<div class="card backup-card" style="margin-top: 32px;">
|
||||||
<h3>💾 Database Backup Management</h3>
|
<h3>💾 Database Backup Management</h3>
|
||||||
|
|||||||
@@ -597,7 +597,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr class="user-row" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-role="{{ user.role }}" data-modules="{{ (user.get_modules() or []) | tojson }}">
|
<tr class="user-row" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-role="{{ user.role }}" data-modules='{{ (user.get_modules() or []) | tojson }}'>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ user.username }}</strong>
|
<strong>{{ user.username }}</strong>
|
||||||
</td>
|
</td>
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
data-user-id="{{ user.id }}"
|
data-user-id="{{ user.id }}"
|
||||||
data-username="{{ user.username }}"
|
data-username="{{ user.username }}"
|
||||||
data-role="{{ user.role }}"
|
data-role="{{ user.role }}"
|
||||||
data-modules="{{ (user.get_modules() or []) | tojson }}"
|
data-modules='{{ (user.get_modules() or []) | tojson }}'
|
||||||
title="Select for quick edit">
|
title="Select for quick edit">
|
||||||
📝 Select
|
📝 Select
|
||||||
</button>
|
</button>
|
||||||
@@ -646,7 +646,7 @@
|
|||||||
data-user-id="{{ user.id }}"
|
data-user-id="{{ user.id }}"
|
||||||
data-username="{{ user.username }}"
|
data-username="{{ user.username }}"
|
||||||
data-role="{{ user.role }}"
|
data-role="{{ user.role }}"
|
||||||
data-modules="{{ (user.get_modules() or []) | tojson }}"
|
data-modules='{{ (user.get_modules() or []) | tojson }}'
|
||||||
title="Full edit">
|
title="Full edit">
|
||||||
⚙️ Edit
|
⚙️ Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -798,6 +798,8 @@ function updateEditModuleSelection() {
|
|||||||
let selectedUser = null;
|
let selectedUser = null;
|
||||||
|
|
||||||
function selectUserForQuickEdit(userId, username, role, modules) {
|
function selectUserForQuickEdit(userId, username, role, modules) {
|
||||||
|
console.log('Selecting user:', {userId, username, role, modules});
|
||||||
|
|
||||||
selectedUser = {id: userId, username: username, role: role, modules: modules};
|
selectedUser = {id: userId, username: username, role: role, modules: modules};
|
||||||
|
|
||||||
// Update the quick edit panel
|
// Update the quick edit panel
|
||||||
@@ -806,25 +808,49 @@ function selectUserForQuickEdit(userId, username, role, modules) {
|
|||||||
|
|
||||||
// Clear all module checkboxes first
|
// Clear all module checkboxes first
|
||||||
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]');
|
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]');
|
||||||
checkboxes.forEach(cb => cb.checked = false);
|
checkboxes.forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
console.log('Cleared checkbox:', cb.id);
|
||||||
|
});
|
||||||
|
|
||||||
// Check the appropriate modules
|
// Check the appropriate modules
|
||||||
if (modules && Array.isArray(modules)) {
|
if (modules && Array.isArray(modules) && modules.length > 0) {
|
||||||
|
console.log('User has modules:', modules);
|
||||||
modules.forEach(module => {
|
modules.forEach(module => {
|
||||||
const checkbox = document.getElementById('quick_module_' + module);
|
const checkbox = document.getElementById('quick_module_' + module);
|
||||||
if (checkbox) checkbox.checked = true;
|
console.log('Looking for checkbox:', 'quick_module_' + module, 'Found:', checkbox);
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
console.log('Checked module:', module);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('No modules found for user or empty array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable checkboxes for superadmin and admin roles
|
||||||
|
if (role === 'superadmin' || role === 'admin') {
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.disabled = true;
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.disabled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the quick edit panel
|
// Show the quick edit panel
|
||||||
document.getElementById('quickModuleEdit').style.display = 'block';
|
document.getElementById('quickModuleEdit').style.display = 'block';
|
||||||
document.querySelector('#userRightsPanel .text-center').style.display = 'none';
|
const emptyMessage = document.querySelector('#userRightsPanel .text-center');
|
||||||
|
if (emptyMessage) emptyMessage.style.display = 'none';
|
||||||
|
|
||||||
// Highlight selected row
|
// Highlight selected row
|
||||||
document.querySelectorAll('.user-row').forEach(row => {
|
document.querySelectorAll('.user-row').forEach(row => {
|
||||||
row.classList.remove('table-warning');
|
row.classList.remove('table-warning');
|
||||||
});
|
});
|
||||||
document.querySelector(`[data-user-id="${userId}"]`).classList.add('table-warning');
|
const selectedRow = document.querySelector(`[data-user-id="${userId}"]`);
|
||||||
|
if (selectedRow) selectedRow.classList.add('table-warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUserModules() {
|
function updateUserModules() {
|
||||||
@@ -833,39 +859,56 @@ function updateUserModules() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('updateUserModules called for user:', selectedUser);
|
||||||
|
|
||||||
// Get selected modules
|
// Get selected modules
|
||||||
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]:checked');
|
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]:checked');
|
||||||
const modules = Array.from(checkboxes).map(cb => cb.value);
|
const modules = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
|
||||||
|
console.log('Selected modules:', modules);
|
||||||
|
console.log('User role:', selectedUser.role);
|
||||||
|
|
||||||
// Validate modules for the role
|
// Validate modules for the role
|
||||||
if ((selectedUser.role === 'manager' || selectedUser.role === 'worker') && modules.length === 0) {
|
if ((selectedUser.role === 'manager' || selectedUser.role === 'worker') && modules.length === 0) {
|
||||||
alert(`${selectedUser.role}s must have at least one module assigned.`);
|
alert(`${selectedUser.role}s must have at least one module assigned.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and submit form
|
// Show confirmation
|
||||||
const form = document.createElement('form');
|
const modulesList = modules.length > 0 ? modules.join(', ') : 'None';
|
||||||
form.method = 'POST';
|
if (!confirm(`Update modules for user "${selectedUser.username}"?\n\nNew modules: ${modulesList}`)) {
|
||||||
form.action = '/quick_update_modules';
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add user ID
|
// Use fetch to submit and see the response
|
||||||
const userIdInput = document.createElement('input');
|
const formData = new FormData();
|
||||||
userIdInput.type = 'hidden';
|
formData.append('user_id', selectedUser.id);
|
||||||
userIdInput.name = 'user_id';
|
|
||||||
userIdInput.value = selectedUser.id;
|
|
||||||
form.appendChild(userIdInput);
|
|
||||||
|
|
||||||
// Add modules
|
|
||||||
modules.forEach(module => {
|
modules.forEach(module => {
|
||||||
const input = document.createElement('input');
|
formData.append('modules', module);
|
||||||
input.type = 'hidden';
|
|
||||||
input.name = 'modules';
|
|
||||||
input.value = module;
|
|
||||||
form.appendChild(input);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(form);
|
console.log('Submitting via fetch to /quick_update_modules');
|
||||||
form.submit();
|
console.log('FormData entries:', Array.from(formData.entries()));
|
||||||
|
|
||||||
|
fetch('/quick_update_modules', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response headers:', Array.from(response.headers.entries()));
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(html => {
|
||||||
|
console.log('Response HTML length:', html.length);
|
||||||
|
console.log('Response HTML preview:', html.substring(0, 500));
|
||||||
|
// Reload the page to show updated data
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fetch error:', error);
|
||||||
|
alert('Error updating modules: ' + error.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFullEditModal() {
|
function showFullEditModal() {
|
||||||
@@ -956,9 +999,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const role = this.dataset.role;
|
const role = this.dataset.role;
|
||||||
let modules = [];
|
let modules = [];
|
||||||
try {
|
try {
|
||||||
modules = JSON.parse(this.dataset.modules) || [];
|
const modulesData = this.dataset.modules;
|
||||||
|
console.log('Raw modules data:', modulesData);
|
||||||
|
modules = JSON.parse(modulesData) || [];
|
||||||
|
console.log('Parsed modules:', modules);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error parsing modules for user:', username, e);
|
console.error('Error parsing modules for user:', username, e);
|
||||||
modules = [];
|
modules = [];
|
||||||
}
|
}
|
||||||
selectUserForQuickEdit(userId, username, role, modules);
|
selectUserForQuickEdit(userId, username, role, modules);
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ def get_db_connection():
|
|||||||
settings = {}
|
settings = {}
|
||||||
with open(settings_file, 'r') as f:
|
with open(settings_file, 'r') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
key, value = line.strip().split('=', 1)
|
line = line.strip()
|
||||||
settings[key] = value
|
if line and '=' in line and not line.startswith('#'):
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
settings[key] = value
|
||||||
return mariadb.connect(
|
return mariadb.connect(
|
||||||
user=settings['username'],
|
user=settings['username'],
|
||||||
password=settings['password'],
|
password=settings['password'],
|
||||||
@@ -97,16 +99,22 @@ def delete_locations_by_ids(ids_str):
|
|||||||
return f"Deleted {deleted} location(s)."
|
return f"Deleted {deleted} location(s)."
|
||||||
|
|
||||||
def create_locations_handler():
|
def create_locations_handler():
|
||||||
message = None
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.form.get("delete_locations"):
|
if request.form.get("delete_locations"):
|
||||||
ids_str = request.form.get("delete_ids", "")
|
ids_str = request.form.get("delete_ids", "")
|
||||||
message = delete_locations_by_ids(ids_str)
|
message = delete_locations_by_ids(ids_str)
|
||||||
|
session['flash_message'] = message
|
||||||
else:
|
else:
|
||||||
location_code = request.form.get("location_code")
|
location_code = request.form.get("location_code")
|
||||||
size = request.form.get("size")
|
size = request.form.get("size")
|
||||||
description = request.form.get("description")
|
description = request.form.get("description")
|
||||||
message = add_location(location_code, size, description)
|
message = add_location(location_code, size, description)
|
||||||
|
session['flash_message'] = message
|
||||||
|
# Redirect to prevent form resubmission on page reload
|
||||||
|
return redirect(url_for('warehouse.create_locations'))
|
||||||
|
|
||||||
|
# Get flash message from session if any
|
||||||
|
message = session.pop('flash_message', None)
|
||||||
locations = get_locations()
|
locations = get_locations()
|
||||||
return render_template("create_locations.html", locations=locations, message=message)
|
return render_template("create_locations.html", locations=locations, message=message)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user