docs: Add comprehensive settings page analysis and improvements

- Add detailed settings page analysis report (settings.md)
- Document identified security vulnerabilities and code quality issues
- Provide prioritized improvement recommendations
- Document permission and access control issues
- Add testing checklist for validation
- Track modifications to settings.py, routes.py, and settings.html templates
This commit is contained in:
Quality App System
2026-01-23 22:54:11 +02:00
parent 64b67b2979
commit d45dc1dab1
8 changed files with 1969 additions and 600 deletions

View File

@@ -22,11 +22,7 @@ from app.settings import (
save_role_permissions_handler,
reset_role_permissions_handler,
save_all_role_permissions_handler,
reset_all_role_permissions_handler,
edit_user_handler,
create_user_handler,
delete_user_handler,
save_external_db_handler
reset_all_role_permissions_handler
)
from .print_module import get_unprinted_orders_data, get_printed_orders_data
from .access_control import (
@@ -398,18 +394,17 @@ def create_user_simple():
# Add to external database
with db_connection_context() as conn:
cursor = conn.cursor()
# Check if user already exists
cursor.execute("SELECT username FROM users WHERE username=%s", (username,))
if cursor.fetchone():
flash(f'User "{username}" already exists.')
conn.close()
return redirect(url_for('main.user_management_simple'))
# Insert new user
cursor.execute("INSERT INTO users (username, password, role, modules) VALUES (%s, %s, %s, %s)",
(username, password, role, modules_json))
conn.commit()
# Check if user already exists
cursor.execute("SELECT username FROM users WHERE username=%s", (username,))
if cursor.fetchone():
flash(f'User "{username}" already exists.')
return redirect(url_for('main.user_management_simple'))
# Insert new user
cursor.execute("INSERT INTO users (username, password, role, modules) VALUES (%s, %s, %s, %s)",
(username, password, role, modules_json))
conn.commit()
flash(f'User "{username}" created successfully as {role}.')
return redirect(url_for('main.user_management_simple'))
@@ -450,23 +445,22 @@ def edit_user_simple():
# Update in external database
with db_connection_context() as conn:
cursor = conn.cursor()
# Check if username is taken by another user
cursor.execute("SELECT id FROM users WHERE username=%s AND id!=%s", (username, user_id))
if cursor.fetchone():
flash(f'Username "{username}" is already taken.')
conn.close()
return redirect(url_for('main.user_management_simple'))
# Update user
if password:
cursor.execute("UPDATE users SET username=%s, password=%s, role=%s, modules=%s WHERE id=%s",
(username, password, role, modules_json, user_id))
else:
cursor.execute("UPDATE users SET username=%s, role=%s, modules=%s WHERE id=%s",
(username, role, modules_json, user_id))
conn.commit()
# Check if username is taken by another user
cursor.execute("SELECT id FROM users WHERE username=%s AND id!=%s", (username, user_id))
if cursor.fetchone():
flash(f'Username "{username}" is already taken.')
return redirect(url_for('main.user_management_simple'))
# Update user
if password:
cursor.execute("UPDATE users SET username=%s, password=%s, role=%s, modules=%s WHERE id=%s",
(username, password, role, modules_json, user_id))
else:
cursor.execute("UPDATE users SET username=%s, role=%s, modules=%s WHERE id=%s",
(username, role, modules_json, user_id))
conn.commit()
flash(f'User "{username}" updated successfully.')
return redirect(url_for('main.user_management_simple'))
@@ -490,15 +484,15 @@ def delete_user_simple():
# Delete from external database
with db_connection_context() as conn:
cursor = conn.cursor()
# Get username before deleting
cursor.execute("SELECT username FROM users WHERE id=%s", (user_id,))
row = cursor.fetchone()
username = row[0] if row else 'Unknown'
# Delete user
cursor.execute("DELETE FROM users WHERE id=%s", (user_id,))
conn.commit()
# Get username before deleting
cursor.execute("SELECT username FROM users WHERE id=%s", (user_id,))
row = cursor.fetchone()
username = row[0] if row else 'Unknown'
# Delete user
cursor.execute("DELETE FROM users WHERE id=%s", (user_id,))
conn.commit()
flash(f'User "{username}" deleted successfully.')
return redirect(url_for('main.user_management_simple'))
@@ -523,38 +517,36 @@ def quick_update_modules():
# Get current user to validate role
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("SELECT username, role, modules FROM users WHERE id=%s", (user_id,))
user_row = cursor.fetchone()
if not user_row:
flash('User not found.')
conn.close()
return redirect(url_for('main.user_management_simple'))
username, role, current_modules = user_row
# Validate modules for the role
from app.permissions_simple import validate_user_modules
is_valid, error_msg = validate_user_modules(role, modules)
if not is_valid:
flash(f'Invalid module assignment: {error_msg}')
conn.close()
return redirect(url_for('main.user_management_simple'))
# Prepare modules JSON
modules_json = None
if modules and role in ['manager', 'worker']:
import json
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
cursor.execute("UPDATE users SET modules=%s WHERE id=%s", (modules_json, user_id))
conn.commit()
cursor.execute("SELECT username, role, modules FROM users WHERE id=%s", (user_id,))
user_row = cursor.fetchone()
if not user_row:
flash('User not found.')
return redirect(url_for('main.user_management_simple'))
username, role, current_modules = user_row
# Validate modules for the role
from app.permissions_simple import validate_user_modules
is_valid, error_msg = validate_user_modules(role, modules)
if not is_valid:
flash(f'Invalid module assignment: {error_msg}')
return redirect(url_for('main.user_management_simple'))
# Prepare modules JSON
modules_json = None
if modules and role in ['manager', 'worker']:
import json
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
cursor.execute("UPDATE users SET modules=%s WHERE id=%s", (modules_json, user_id))
conn.commit()
flash(f'Modules updated successfully for user "{username}". New modules: {", ".join(modules) if modules else "None"}', 'success')
@@ -606,31 +598,31 @@ def scan():
with db_connection_context() as conn:
cursor = conn.cursor()
# Insert new entry - the BEFORE INSERT trigger 'set_quantities_scan1' will automatically
# calculate and set approved_quantity and rejected_quantity for this new record
insert_query = """
INSERT INTO scan1_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
conn.commit()
# Get the quantities from the newly inserted row for the flash message
cp_base_code = cp_code[:10]
cursor.execute("""
SELECT approved_quantity, rejected_quantity
FROM scan1_orders
WHERE CP_full_code = %s
""", (cp_code,))
result = cursor.fetchone()
approved_count = result[0] if result else 0
rejected_count = result[1] if result else 0
# Flash appropriate message
if int(defect_code) == 0:
flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
else:
flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
# Insert new entry - the BEFORE INSERT trigger 'set_quantities_scan1' will automatically
# calculate and set approved_quantity and rejected_quantity for this new record
insert_query = """
INSERT INTO scan1_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
conn.commit()
# Get the quantities from the newly inserted row for the flash message
cp_base_code = cp_code[:10]
cursor.execute("""
SELECT approved_quantity, rejected_quantity
FROM scan1_orders
WHERE CP_full_code = %s
""", (cp_code,))
result = cursor.fetchone()
approved_count = result[0] if result else 0
rejected_count = result[1] if result else 0
# Flash appropriate message
if int(defect_code) == 0:
flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
else:
flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
except mariadb.Error as e:
@@ -642,15 +634,15 @@ def scan():
try:
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
ORDER BY Id DESC
LIMIT 15
""")
raw_scan_data = cursor.fetchall()
# Apply formatting to scan data for consistent date display
scan_data = [[format_cell_data(cell) for cell in row] for row in raw_scan_data]
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
ORDER BY Id DESC
LIMIT 15
""")
raw_scan_data = cursor.fetchall()
# Apply formatting to scan data for consistent date display
scan_data = [[format_cell_data(cell) for cell in row] for row in raw_scan_data]
except mariadb.Error as e:
print(f"Error fetching scan data: {e}")
flash(f"Error fetching scan data: {e}")
@@ -685,32 +677,32 @@ def fg_scan():
with db_connection_context() as conn:
cursor = conn.cursor()
# Always insert a new entry - each scan is a separate record
# Note: The trigger 'increment_approved_quantity_fg' will automatically
# update approved_quantity or rejected_quantity for all records with same CP_base_code
insert_query = """
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
conn.commit()
# Get the quantities from the newly inserted row for the flash message
cp_base_code = cp_code[:10]
cursor.execute("""
SELECT approved_quantity, rejected_quantity
FROM scanfg_orders
WHERE CP_full_code = %s
""", (cp_code,))
result = cursor.fetchone()
approved_count = result[0] if result else 0
rejected_count = result[1] if result else 0
# Flash appropriate message
if int(defect_code) == 0:
flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
else:
flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
# Always insert a new entry - each scan is a separate record
# Note: The trigger 'increment_approved_quantity_fg' will automatically
# update approved_quantity or rejected_quantity for all records with same CP_base_code
insert_query = """
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
conn.commit()
# Get the quantities from the newly inserted row for the flash message
cp_base_code = cp_code[:10]
cursor.execute("""
SELECT approved_quantity, rejected_quantity
FROM scanfg_orders
WHERE CP_full_code = %s
""", (cp_code,))
result = cursor.fetchone()
approved_count = result[0] if result else 0
rejected_count = result[1] if result else 0
# Flash appropriate message
if int(defect_code) == 0:
flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
else:
flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
except mariadb.Error as e:
@@ -730,37 +722,21 @@ def fg_scan():
try:
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scanfg_orders
ORDER BY Id DESC
LIMIT 15
""")
raw_scan_data = cursor.fetchall()
# Apply formatting to scan data for consistent date display
scan_data = [[format_cell_data(cell) for cell in row] for row in raw_scan_data]
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scanfg_orders
ORDER BY Id DESC
LIMIT 15
""")
raw_scan_data = cursor.fetchall()
# Apply formatting to scan data for consistent date display
scan_data = [[format_cell_data(cell) for cell in row] for row in raw_scan_data]
except mariadb.Error as e:
print(f"Error fetching finish goods scan data: {e}")
flash(f"Error fetching scan data: {e}")
return render_template('fg_scan.html', scan_data=scan_data)
@bp.route('/create_user', methods=['POST'])
def create_user():
return create_user_handler()
@bp.route('/edit_user', methods=['POST'])
def edit_user():
return edit_user_handler()
@bp.route('/delete_user', methods=['POST'])
def delete_user():
return delete_user_handler()
@bp.route('/save_external_db', methods=['POST'])
def save_external_db():
return save_external_db_handler()
# Role Permissions Management Routes
@bp.route('/role_permissions')
@superadmin_only
@@ -917,90 +893,90 @@ def get_report_data():
with db_connection_context() as conn:
cursor = conn.cursor()
if report == "1": # Logic for the 1-day report (today's records)
today = datetime.now().strftime('%Y-%m-%d')
print(f"DEBUG: Daily report searching for records on date: {today}")
cursor.execute("""
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date = ?
ORDER BY date DESC, time DESC
""", (today,))
rows = cursor.fetchall()
print(f"DEBUG: Daily report found {len(rows)} rows for today ({today}):", rows)
data["headers"] = ["Id", "Operator Code", "CP Base Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
if report == "1": # Logic for the 1-day report (today's records)
today = datetime.now().strftime('%Y-%m-%d')
print(f"DEBUG: Daily report searching for records on date: {today}")
cursor.execute("""
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date = ?
ORDER BY date DESC, time DESC
""", (today,))
rows = cursor.fetchall()
print(f"DEBUG: Daily report found {len(rows)} rows for today ({today}):", rows)
data["headers"] = ["Id", "Operator Code", "CP Base Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "2": # Logic for the 5-day report (last 5 days including today)
five_days_ago = datetime.now() - timedelta(days=4) # Last 4 days + today = 5 days
start_date = five_days_ago.strftime('%Y-%m-%d')
print(f"DEBUG: 5-day report searching for records from {start_date} onwards")
cursor.execute("""
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date >= ?
ORDER BY date DESC, time DESC
""", (start_date,))
rows = cursor.fetchall()
print(f"DEBUG: 5-day report found {len(rows)} rows from {start_date} onwards:", rows)
data["headers"] = ["Id", "Operator Code", "CP Base Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "2": # Logic for the 5-day report (last 5 days including today)
five_days_ago = datetime.now() - timedelta(days=4) # Last 4 days + today = 5 days
start_date = five_days_ago.strftime('%Y-%m-%d')
print(f"DEBUG: 5-day report searching for records from {start_date} onwards")
cursor.execute("""
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date >= ?
ORDER BY date DESC, time DESC
""", (start_date,))
rows = cursor.fetchall()
print(f"DEBUG: 5-day report found {len(rows)} rows from {start_date} onwards:", rows)
data["headers"] = ["Id", "Operator Code", "CP Base Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "3": # Logic for the report with non-zero quality_code (today only)
today = datetime.now().strftime('%Y-%m-%d')
print(f"DEBUG: Quality defects report (today) searching for records on {today} with quality issues")
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date = ? AND quality_code != 0
ORDER BY date DESC, time DESC
""", (today,))
rows = cursor.fetchall()
print(f"DEBUG: Quality defects report (today) found {len(rows)} rows with quality issues for {today}:", rows)
data["headers"] = ["Id", "Operator Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "3": # Logic for the report with non-zero quality_code (today only)
today = datetime.now().strftime('%Y-%m-%d')
print(f"DEBUG: Quality defects report (today) searching for records on {today} with quality issues")
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date = ? AND quality_code != 0
ORDER BY date DESC, time DESC
""", (today,))
rows = cursor.fetchall()
print(f"DEBUG: Quality defects report (today) found {len(rows)} rows with quality issues for {today}:", rows)
data["headers"] = ["Id", "Operator Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "4": # Logic for the report with non-zero quality_code (last 5 days)
five_days_ago = datetime.now() - timedelta(days=4) # Last 4 days + today = 5 days
start_date = five_days_ago.strftime('%Y-%m-%d')
print(f"DEBUG: Quality defects report (5 days) searching for records from {start_date} onwards with quality issues")
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date >= ? AND quality_code != 0
ORDER BY date DESC, time DESC
""", (start_date,))
rows = cursor.fetchall()
print(f"DEBUG: Quality defects report (5 days) found {len(rows)} rows with quality issues from {start_date} onwards:", rows)
data["headers"] = ["Id", "Operator Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "4": # Logic for the report with non-zero quality_code (last 5 days)
five_days_ago = datetime.now() - timedelta(days=4) # Last 4 days + today = 5 days
start_date = five_days_ago.strftime('%Y-%m-%d')
print(f"DEBUG: Quality defects report (5 days) searching for records from {start_date} onwards with quality issues")
cursor.execute("""
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
WHERE date >= ? AND quality_code != 0
ORDER BY date DESC, time DESC
""", (start_date,))
rows = cursor.fetchall()
print(f"DEBUG: Quality defects report (5 days) found {len(rows)} rows with quality issues from {start_date} onwards:", rows)
data["headers"] = ["Id", "Operator Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity", "Rejected Quantity"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "5": # Logic for the 5-ft report (all rows)
# First check if table exists and has any data
try:
cursor.execute("SELECT COUNT(*) FROM scan1_orders")
total_count = cursor.fetchone()[0]
print(f"DEBUG: Total records in scan1_orders table: {total_count}")
if total_count == 0:
print("DEBUG: No data found in scan1_orders table")
data["headers"] = ["Id", "Operator Code", "CP Base Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity of order", "Rejected Quantity of order"]
data["rows"] = []
data["message"] = "No scan data available in the database. Please ensure scanning operations have been performed and data has been recorded."
else:
cursor.execute("""
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
ORDER BY date DESC, time DESC
""")
rows = cursor.fetchall()
print(f"DEBUG: Fetched {len(rows)} rows for report 5 (all rows)")
data["headers"] = ["Id", "Operator Code", "CP Base Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity of order", "Rejected Quantity of order"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
elif report == "5": # Logic for the 5-ft report (all rows)
# First check if table exists and has any data
try:
cursor.execute("SELECT COUNT(*) FROM scan1_orders")
total_count = cursor.fetchone()[0]
print(f"DEBUG: Total records in scan1_orders table: {total_count}")
except mariadb.Error as table_error:
print(f"DEBUG: Table access error: {table_error}")
data["error"] = f"Database table error: {table_error}"
if total_count == 0:
print("DEBUG: No data found in scan1_orders table")
data["headers"] = ["Id", "Operator Code", "CP Base Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity of order", "Rejected Quantity of order"]
data["rows"] = []
data["message"] = "No scan data available in the database. Please ensure scanning operations have been performed and data has been recorded."
else:
cursor.execute("""
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
ORDER BY date DESC, time DESC
""")
rows = cursor.fetchall()
print(f"DEBUG: Fetched {len(rows)} rows for report 5 (all rows)")
data["headers"] = ["Id", "Operator Code", "CP Base Code", "CP Full Code", "OC1 Code", "OC2 Code", "Quality Code", "Date", "Time", "Approved Quantity of order", "Rejected Quantity of order"]
data["rows"] = [[format_cell_data(cell) for cell in row] for row in rows]
except mariadb.Error as table_error:
print(f"DEBUG: Table access error: {table_error}")
data["error"] = f"Database table error: {table_error}"
except mariadb.Error as e:
print(f"Error fetching report data: {e}")
@@ -1277,19 +1253,18 @@ def debug_dates():
try:
with db_connection_context() as conn:
cursor = conn.cursor()
# Get all distinct dates
cursor.execute("SELECT DISTINCT date FROM scan1_orders ORDER BY date DESC")
dates = cursor.fetchall()
# Get total count
cursor.execute("SELECT COUNT(*) FROM scan1_orders")
total_count = cursor.fetchone()[0]
# Get sample data
cursor.execute("SELECT date, time FROM scan1_orders ORDER BY date DESC LIMIT 5")
sample_data = cursor.fetchall()
# Get all distinct dates
cursor.execute("SELECT DISTINCT date FROM scan1_orders ORDER BY date DESC")
dates = cursor.fetchall()
# Get total count
cursor.execute("SELECT COUNT(*) FROM scan1_orders")
total_count = cursor.fetchone()[0]
# Get sample data
cursor.execute("SELECT date, time FROM scan1_orders ORDER BY date DESC LIMIT 5")
sample_data = cursor.fetchall()
return jsonify({
"total_records": total_count,
@@ -2408,21 +2383,21 @@ def view_orders():
# Get all orders data (not just unprinted)
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
created_at, updated_at, printed_labels, data_livrare, dimensiune
FROM order_for_labels
ORDER BY created_at DESC
LIMIT 500
""")
orders_data = []
for row in cursor.fetchall():
orders_data.append({
'id': row[0],
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
created_at, updated_at, printed_labels, data_livrare, dimensiune
FROM order_for_labels
ORDER BY created_at DESC
LIMIT 500
""")
orders_data = []
for row in cursor.fetchall():
orders_data.append({
'id': row[0],
'comanda_productie': row[1],
'cod_articol': row[2],
'descr_com_prod': row[3],
@@ -3658,17 +3633,17 @@ def generate_labels_pdf(order_id, paper_saving_mode='true'):
# Get order data from database
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
printed_labels, created_at, updated_at
FROM order_for_labels
WHERE id = %s
""", (order_id,))
row = cursor.fetchone()
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
printed_labels, created_at, updated_at
FROM order_for_labels
WHERE id = %s
""", (order_id,))
row = cursor.fetchone()
if not row:
return jsonify({'error': 'Order not found'}), 404
@@ -4025,17 +4000,17 @@ def get_order_data(order_id):
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
printed_labels, created_at, updated_at
FROM order_for_labels
WHERE id = %s
""", (order_id,))
row = cursor.fetchone()
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
printed_labels, created_at, updated_at
FROM order_for_labels
WHERE id = %s
""", (order_id,))
row = cursor.fetchone()
if not row:
return jsonify({'error': 'Order not found'}), 404
@@ -4080,22 +4055,21 @@ def mark_printed():
# Connect to the database and update the printed status
with db_connection_context() as conn:
cursor = conn.cursor()
# Update the order to mark it as printed
update_query = """
UPDATE orders_for_labels
SET printed_labels = printed_labels + 1,
updated_at = NOW()
WHERE id = %s
"""
cursor.execute(update_query, (order_id,))
if cursor.rowcount == 0:
conn.close()
return jsonify({'error': 'Order not found'}), 404
conn.commit()
# Update the order to mark it as printed
update_query = """
UPDATE orders_for_labels
SET printed_labels = printed_labels + 1,
updated_at = NOW()
WHERE id = %s
"""
cursor.execute(update_query, (order_id,))
if cursor.rowcount == 0:
return jsonify({'error': 'Order not found'}), 404
conn.commit()
return jsonify({'success': True, 'message': 'Order marked as printed'})
@@ -5072,6 +5046,119 @@ def get_storage_info():
}), 500
@bp.route('/log_explorer')
@admin_plus
def log_explorer():
"""Display log explorer page"""
return render_template('log_explorer.html')
@bp.route('/api/logs/list', methods=['GET'])
@admin_plus
def get_logs_list():
"""Get list of all log files"""
import os
import glob
logs_dir = '/srv/quality_app/logs'
if not os.path.exists(logs_dir):
return jsonify({'success': True, 'logs': []})
log_files = []
for log_file in sorted(glob.glob(os.path.join(logs_dir, '*.log*')), reverse=True):
try:
stat = os.stat(log_file)
log_files.append({
'name': os.path.basename(log_file),
'size': stat.st_size,
'size_formatted': format_size_for_json(stat.st_size),
'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
'path': log_file
})
except:
continue
return jsonify({'success': True, 'logs': log_files})
@bp.route('/api/logs/view/<filename>', methods=['GET'])
@admin_plus
def view_log_file(filename):
"""View contents of a specific log file with pagination"""
import os
# Security: prevent directory traversal
if '..' in filename or '/' in filename:
return jsonify({'success': False, 'message': 'Invalid filename'}), 400
logs_dir = '/srv/quality_app/logs'
log_path = os.path.join(logs_dir, filename)
# Verify the file is in the logs directory
if not os.path.abspath(log_path).startswith(os.path.abspath(logs_dir)):
return jsonify({'success': False, 'message': 'Invalid file path'}), 400
if not os.path.exists(log_path):
return jsonify({'success': False, 'message': 'Log file not found'}), 404
try:
lines_per_page = request.args.get('lines', 100, type=int)
page = request.args.get('page', 1, type=int)
# Limit lines per page
if lines_per_page < 10:
lines_per_page = 10
if lines_per_page > 1000:
lines_per_page = 1000
with open(log_path, 'r', encoding='utf-8', errors='ignore') as f:
all_lines = f.readlines()
total_lines = len(all_lines)
total_pages = (total_lines + lines_per_page - 1) // lines_per_page
# Ensure page is valid
if page < 1:
page = 1
if page > total_pages and total_pages > 0:
page = total_pages
# Get lines for current page (show from end, latest lines first)
start_idx = total_lines - (page * lines_per_page)
end_idx = total_lines - ((page - 1) * lines_per_page)
if start_idx < 0:
start_idx = 0
current_lines = all_lines[start_idx:end_idx]
current_lines.reverse() # Show latest first
return jsonify({
'success': True,
'filename': filename,
'lines': current_lines,
'current_page': page,
'total_pages': total_pages,
'total_lines': total_lines,
'lines_per_page': lines_per_page
})
except Exception as e:
return jsonify({'success': False, 'message': f'Error reading log: {str(e)}'}), 500
def format_size_for_json(size_bytes):
"""Format bytes to human readable size for JSON responses"""
if size_bytes >= 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
elif size_bytes >= 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.2f} MB"
elif size_bytes >= 1024:
return f"{size_bytes / 1024:.2f} KB"
else:
return f"{size_bytes} bytes"
@bp.route('/api/maintenance/database-tables', methods=['GET'])
@admin_plus
def get_all_database_tables():
@@ -5594,8 +5681,8 @@ def api_assign_box_to_location():
try:
with db_connection_context() as conn:
cursor = conn.cursor()
cursor.execute("SELECT status FROM boxes_crates WHERE id = %s", (box_id,))
result = cursor.fetchone()
cursor.execute("SELECT status FROM boxes_crates WHERE id = %s", (box_id,))
result = cursor.fetchone()
if result and result[0] == 'open':
return jsonify({

View File

@@ -197,8 +197,8 @@ def role_permissions_handler():
def settings_handler():
if 'role' not in session or session['role'] != 'superadmin':
flash('Access denied: Superadmin only.')
if 'role' not in session or session['role'] not in ['superadmin', 'admin']:
flash('Access denied: Admin or Superadmin required.')
return redirect(url_for('main.dashboard'))
# Get users from external MariaDB database
@@ -265,185 +265,6 @@ def get_external_db_connection():
return get_db_connection()
# User management handlers
def create_user_handler():
if 'role' not in session or session['role'] != 'superadmin':
flash('Access denied: Superadmin only.')
return redirect(url_for('main.settings'))
username = request.form['username']
password = request.form['password']
role = request.form['role']
email = request.form.get('email', '').strip() or None # Optional field
try:
# Connect to external MariaDB database
conn = get_external_db_connection()
cursor = conn.cursor()
# Create users table if it doesn't exist - with modules column
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL,
modules JSON DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Ensure modules column exists (for backward compatibility)
try:
cursor.execute("SELECT modules FROM users LIMIT 1")
except mariadb.ProgrammingError:
cursor.execute("ALTER TABLE users ADD COLUMN modules JSON DEFAULT NULL")
# Check if the username already exists
cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
if cursor.fetchone():
flash('User already exists.')
conn.close()
return redirect(url_for('main.settings'))
# Prepare modules based on role
import json
if role == 'superadmin':
# Superadmin doesn't need explicit modules (handled at login)
user_modules = None
elif role == 'admin':
# Admin gets access to all available modules
user_modules = json.dumps(['quality', 'warehouse', 'labels', 'daily_mirror'])
else:
# Other roles (manager, worker) get no modules by default
user_modules = json.dumps([])
# Create a new user in external MariaDB with modules
cursor.execute("""
INSERT INTO users (username, password, role, modules)
VALUES (%s, %s, %s, %s)
""", (username, password, role, user_modules))
conn.commit()
conn.close()
flash('User created successfully in external database.')
except Exception as e:
print(f"Error creating user in external database: {e}")
flash(f'Error creating user: {e}')
return redirect(url_for('main.settings'))
def edit_user_handler():
if 'role' not in session or session['role'] != 'superadmin':
flash('Access denied: Superadmin only.')
return redirect(url_for('main.settings'))
user_id = request.form.get('user_id')
password = request.form.get('password', '').strip()
role = request.form.get('role')
modules = request.form.getlist('modules') # Get selected modules
if not user_id or not role:
flash('Missing required fields.')
return redirect(url_for('main.settings'))
try:
# Connect to external MariaDB database
conn = get_external_db_connection()
cursor = conn.cursor()
# Check if the user exists
cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
if not cursor.fetchone():
flash('User not found.')
conn.close()
return redirect(url_for('main.settings'))
# Prepare modules JSON
import json
if role == 'superadmin':
user_modules = None # Superadmin doesn't need explicit modules
else:
user_modules = json.dumps(modules) if modules else json.dumps([])
# Update the user's details in external MariaDB
if password: # Only update password if provided
cursor.execute("""
UPDATE users SET password = %s, role = %s, modules = %s WHERE id = %s
""", (password, role, user_modules, user_id))
flash('User updated successfully (including password).')
else: # Just update role and modules if no password provided
cursor.execute("""
UPDATE users SET role = %s, modules = %s WHERE id = %s
""", (role, user_modules, user_id))
flash('User role and modules updated successfully.')
conn.commit()
conn.close()
except Exception as e:
print(f"Error updating user in external database: {e}")
flash(f'Error updating user: {e}')
return redirect(url_for('main.settings'))
def delete_user_handler():
if 'role' not in session or session['role'] != 'superadmin':
flash('Access denied: Superadmin only.')
return redirect(url_for('main.settings'))
user_id = request.form['user_id']
try:
# Connect to external MariaDB database
conn = get_external_db_connection()
cursor = conn.cursor()
# Check if the user exists
cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
if not cursor.fetchone():
flash('User not found.')
conn.close()
return redirect(url_for('main.settings'))
# Delete the user from external MariaDB
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
conn.commit()
conn.close()
flash('User deleted successfully from external database.')
except Exception as e:
print(f"Error deleting user from external database: {e}")
flash(f'Error deleting user: {e}')
return redirect(url_for('main.settings'))
def save_external_db_handler():
if 'role' not in session or session['role'] != 'superadmin':
flash('Access denied: Superadmin only.')
return redirect(url_for('main.settings'))
# Get form data
server_domain = request.form['server_domain']
port = request.form['port']
database_name = request.form['database_name']
username = request.form['username']
password = request.form['password']
# Save data to a file in the instance folder
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
os.makedirs(os.path.dirname(settings_file), exist_ok=True)
with open(settings_file, 'w') as f:
f.write(f"server_domain={server_domain}\n")
f.write(f"port={port}\n")
f.write(f"database_name={database_name}\n")
f.write(f"username={username}\n")
f.write(f"password={password}\n")
flash('External database settings saved/updated successfully.')
return redirect(url_for('main.settings'))
def save_role_permissions_handler():
"""Save role permissions via AJAX"""
if not is_superadmin():

View File

@@ -0,0 +1,252 @@
{% extends "base.html" %}
{% block title %}Log Explorer{% endblock %}
{% block content %}
<div style="padding: 20px; max-width: 1400px; margin: 0 auto;">
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 30px;">
<h1 style="margin: 0; color: var(--text-primary, #333); font-size: 2em;">📋 Log Explorer</h1>
<span style="background: var(--accent-color, #4caf50); color: white; padding: 6px 12px; border-radius: 6px; font-size: 0.85em; font-weight: 600;">Admin</span>
</div>
<div style="display: grid; grid-template-columns: 350px 1fr; gap: 20px; margin-bottom: 20px;">
<!-- Log Files List -->
<div style="background: var(--card-bg, white); border: 1px solid var(--border-color, #ddd); border-radius: 8px; overflow: hidden; display: flex; flex-direction: column;">
<div style="padding: 15px; background: var(--header-bg, #f5f5f5); border-bottom: 1px solid var(--border-color, #ddd); display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.2em;">📁</span>
<strong>Log Files</strong>
</div>
<div id="logs-list" style="flex: 1; overflow-y: auto; padding: 10px; min-height: 400px;">
<div style="text-align: center; padding: 20px; color: var(--text-secondary, #666);">
<div style="font-size: 2em; margin-bottom: 10px;"></div>
<p>Loading log files...</p>
</div>
</div>
<div style="padding: 10px; border-top: 1px solid var(--border-color, #ddd); text-align: center; font-size: 0.85em; color: var(--text-secondary, #666);">
<span id="log-count">0</span> files
</div>
</div>
<!-- Log Content -->
<div style="background: var(--card-bg, white); border: 1px solid var(--border-color, #ddd); border-radius: 8px; overflow: hidden; display: flex; flex-direction: column;">
<div style="padding: 15px; background: var(--header-bg, #f5f5f5); border-bottom: 1px solid var(--border-color, #ddd); display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.2em;">📄</span>
<strong id="selected-log-name">Select a log file to view</strong>
</div>
<button id="download-log-btn" onclick="downloadCurrentLog()" style="display: none; background: #2196f3; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.9em;">
⬇️ Download
</button>
</div>
<div id="log-content" style="flex: 1; overflow-y: auto; padding: 15px; font-family: 'Courier New', monospace; font-size: 0.85em; line-height: 1.5; background: var(--code-bg, #f9f9f9); color: var(--code-text, #333); white-space: pre-wrap; word-wrap: break-word; min-height: 400px;">
<div style="text-align: center; padding: 40px 20px; color: var(--text-secondary, #666);">
<div style="font-size: 2em; margin-bottom: 10px;">📖</div>
<p>Select a log file from the list to view its contents</p>
</div>
</div>
<!-- Pagination -->
<div id="pagination-controls" style="padding: 15px; background: var(--header-bg, #f5f5f5); border-top: 1px solid var(--border-color, #ddd); display: none; text-align: center; gap: 10px; display: flex; align-items: center; justify-content: center;">
<button id="prev-page-btn" onclick="previousPage()" style="background: #2196f3; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600;">
← Previous
</button>
<span id="page-info" style="font-weight: 600; color: var(--text-primary, #333);">Page 1 of 1</span>
<button id="next-page-btn" onclick="nextPage()" style="background: #2196f3; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600;">
Next →
</button>
<span id="lines-info" style="margin-left: auto; font-size: 0.9em; color: var(--text-secondary, #666);">0 total lines</span>
</div>
</div>
</div>
</div>
<script>
let currentLogFile = null;
let currentPage = 1;
let totalPages = 1;
// Load log files list on page load
document.addEventListener('DOMContentLoaded', function() {
loadLogsList();
});
function loadLogsList() {
fetch('/api/logs/list')
.then(response => response.json())
.then(data => {
if (data.success) {
renderLogsList(data.logs);
} else {
document.getElementById('logs-list').innerHTML = '<div style="padding: 20px; color: #d32f2f; text-align: center;">Failed to load logs</div>';
}
})
.catch(error => {
console.error('Error loading logs list:', error);
document.getElementById('logs-list').innerHTML = '<div style="padding: 20px; color: #d32f2f; text-align: center;">Error: ' + error.message + '</div>';
});
}
function renderLogsList(logs) {
const logsList = document.getElementById('logs-list');
if (logs.length === 0) {
logsList.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-secondary, #666);">No log files found</div>';
document.getElementById('log-count').textContent = '0';
return;
}
let html = '';
logs.forEach(log => {
html += `
<div onclick="viewLog('${log.name}')" style="padding: 12px; border-bottom: 1px solid var(--border-color, #ddd); cursor: pointer; transition: all 0.2s; background: var(--item-bg, transparent);" class="log-item" onmouseover="this.style.background='var(--hover-bg, #f0f0f0)'" onmouseout="this.style.background='var(--item-bg, transparent)'">
<div style="display: flex; align-items: center; gap: 8px;">
<span>📄</span>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; color: var(--text-primary, #333); word-break: break-word;">${log.name}</div>
<div style="font-size: 0.8em; color: var(--text-secondary, #666); margin-top: 4px;">
${log.size_formatted}${log.modified}
</div>
</div>
</div>
</div>
`;
});
logsList.innerHTML = html;
document.getElementById('log-count').textContent = logs.length;
}
function viewLog(filename) {
currentLogFile = filename;
currentPage = 1;
loadLogContent(filename);
}
function loadLogContent(filename) {
const logContent = document.getElementById('log-content');
logContent.innerHTML = '<div style="text-align: center; padding: 40px 20px;"><div style="font-size: 2em; margin-bottom: 10px;">⏳</div><p>Loading...</p></div>';
fetch(`/api/logs/view/${encodeURIComponent(filename)}?page=${currentPage}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderLogContent(data);
} else {
logContent.innerHTML = `<div style="color: #d32f2f; padding: 20px;">Error: ${data.message}</div>`;
}
})
.catch(error => {
console.error('Error loading log content:', error);
logContent.innerHTML = `<div style="color: #d32f2f; padding: 20px;">Error loading log: ${error.message}</div>`;
});
}
function renderLogContent(data) {
const logContent = document.getElementById('log-content');
const lines = data.lines || [];
if (lines.length === 0) {
logContent.textContent = '(Empty file)';
} else {
logContent.textContent = lines.join('');
}
// Update pagination
totalPages = data.total_pages;
currentPage = data.current_page;
const paginationControls = document.getElementById('pagination-controls');
if (totalPages > 1) {
paginationControls.style.display = 'flex';
document.getElementById('page-info').textContent = `Page ${currentPage} of ${totalPages}`;
document.getElementById('lines-info').textContent = `${data.total_lines} total lines`;
document.getElementById('prev-page-btn').disabled = currentPage === 1;
document.getElementById('next-page-btn').disabled = currentPage === totalPages;
} else {
paginationControls.style.display = 'none';
}
// Update header
document.getElementById('selected-log-name').textContent = data.filename;
document.getElementById('download-log-btn').style.display = 'block';
}
function previousPage() {
if (currentPage > 1) {
currentPage--;
loadLogContent(currentLogFile);
}
}
function nextPage() {
if (currentPage < totalPages) {
currentPage++;
loadLogContent(currentLogFile);
}
}
function downloadCurrentLog() {
if (!currentLogFile) return;
const link = document.createElement('a');
link.href = `/logs/${currentLogFile}`;
link.download = currentLogFile;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
<style>
#log-content {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
#logs-list {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color, #ccc) var(--scrollbar-bg, #f5f5f5);
}
#logs-list::-webkit-scrollbar {
width: 8px;
}
#logs-list::-webkit-scrollbar-track {
background: var(--scrollbar-bg, #f5f5f5);
}
#logs-list::-webkit-scrollbar-thumb {
background: var(--scrollbar-color, #ccc);
border-radius: 4px;
}
#log-content {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color, #ccc) var(--scrollbar-bg, #f5f5f5);
}
#log-content::-webkit-scrollbar {
width: 8px;
}
#log-content::-webkit-scrollbar-track {
background: var(--scrollbar-bg, #f5f5f5);
}
#log-content::-webkit-scrollbar-thumb {
background: var(--scrollbar-color, #ccc);
border-radius: 4px;
}
@media (max-width: 768px) {
div[style*="display: grid"][style*="grid-template-columns: 350px"] {
grid-template-columns: 1fr !important;
}
}
</style>
{% endblock %}

View File

@@ -4,38 +4,6 @@
{% block content %}
<div class="card-container">
<div class="card">
<h3>Manage Users (Legacy)</h3>
<ul class="user-list">
{% for user in users %}
<li data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">
<span class="user-name">{{ user.username }}</span>
<span class="user-role">Role: {{ user.role }}</span>
<button class="btn edit-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">Edit User</button>
<button class="btn delete-btn delete-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}">Delete User</button>
</li>
{% endfor %}
</ul>
<button id="create-user-btn" class="btn create-btn">Create User</button>
</div>
<div class="card">
<h3>External Server Settings</h3>
<form method="POST" action="{{ url_for('main.save_external_db') }}" class="form-centered">
<label for="db_server_domain">Server Domain/IP Address:</label>
<input type="text" id="db_server_domain" name="server_domain" value="{{ external_settings.get('server_domain', '') }}" required>
<label for="db_port">Port:</label>
<input type="number" id="db_port" name="port" value="{{ external_settings.get('port', '') }}" required>
<label for="db_database_name">Database Name:</label>
<input type="text" id="db_database_name" name="database_name" value="{{ external_settings.get('database_name', '') }}" required>
<label for="db_username">Username:</label>
<input type="text" id="db_username" name="username" value="{{ external_settings.get('username', '') }}" required>
<label for="db_password">Password:</label>
<input type="password" id="db_password" name="password" value="{{ external_settings.get('password', '') }}" required>
<button type="submit" class="btn">Save/Update External Database Info Settings</button>
</form>
</div>
<div class="card" style="margin-top: 32px;">
<h3>🎯 User & Permissions Management</h3>
<p><strong>Simplified 4-Tier System:</strong> Superadmin → Admin → Manager → Worker</p>
@@ -101,6 +69,9 @@
<button id="cleanup-logs-now-btn" class="btn" style="background-color: #ff9800; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
🗑️ Clean Up Logs Now
</button>
<a href="{{ url_for('main.log_explorer') }}" class="btn" style="background-color: #2196f3; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; text-decoration: none; display: inline-block; transition: all 0.3s;">
📖 View & Explore Logs
</a>
</div>
<div id="log-cleanup-status" style="margin-top: 15px; padding: 12px 16px; background: var(--status-bg, #e3f2fd); border-left: 4px solid var(--status-border, #2196f3); border-radius: 4px; display: none; color: var(--text-primary, #333);">
@@ -1469,87 +1440,7 @@
}
</style>
<!-- Popup for creating/editing a user -->
<div id="user-popup" class="popup" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:var(--app-overlay-bg, rgba(30,41,59,0.85)); z-index:9999; align-items:center; justify-content:center;">
<div class="popup-content" style="margin:auto; padding:32px; border-radius:8px; box-shadow:0 2px 8px #333; min-width:320px; max-width:400px; text-align:center;">
<h3 id="user-popup-title">Create/Edit User</h3>
<form id="user-form" method="POST" action="{{ url_for('main.create_user') }}">
<input type="hidden" id="user-id" name="user_id">
<label for="user_username">Username:</label>
<input type="text" id="user_username" name="username" required>
<label for="user_email">Email (Optional):</label>
<input type="email" id="user_email" name="email">
<label for="user_password">Password:</label>
<input type="password" id="user_password" name="password" required>
<label for="user_role">Role:</label>
<select id="user_role" name="role" required>
<option value="superadmin">Superadmin</option>
<option value="admin">Admin</option>
<option value="manager">Manager</option>
<option value="warehouse_manager">Warehouse Manager</option>
<option value="warehouse_worker">Warehouse Worker</option>
<option value="quality_manager">Quality Manager</option>
<option value="quality_worker">Quality Worker</option>
</select>
<button type="submit" class="btn">Save</button>
<button type="button" id="close-user-popup-btn" class="btn cancel-btn">Cancel</button>
</form>
</div>
</div>
<!-- Popup for confirming user deletion -->
<div id="delete-user-popup" class="popup">
<div class="popup-content">
<h3>Do you really want to delete the user <span id="delete-username"></span>?</h3>
<form id="delete-user-form" method="POST" action="{{ url_for('main.delete_user') }}">
<input type="hidden" id="delete-user-id" name="user_id">
<button type="submit" class="btn delete-confirm-btn">Yes</button>
<button type="button" id="close-delete-popup-btn" class="btn cancel-btn">No</button>
</form>
</div>
</div>
<script>
document.getElementById('create-user-btn').onclick = function() {
document.getElementById('user-popup').style.display = 'flex';
document.getElementById('user-popup-title').innerText = 'Create User';
document.getElementById('user-form').reset();
document.getElementById('user-form').setAttribute('action', '{{ url_for("main.create_user") }}');
document.getElementById('user-id').value = '';
document.getElementById('user_password').required = true;
document.getElementById('user_password').placeholder = '';
document.getElementById('user_username').readOnly = false;
};
document.getElementById('close-user-popup-btn').onclick = function() {
document.getElementById('user-popup').style.display = 'none';
};
// Edit User button logic
Array.from(document.getElementsByClassName('edit-user-btn')).forEach(function(btn) {
btn.onclick = function() {
document.getElementById('user-popup').style.display = 'flex';
document.getElementById('user-popup-title').innerText = 'Edit User';
document.getElementById('user-id').value = btn.getAttribute('data-user-id');
document.getElementById('user_username').value = btn.getAttribute('data-username');
document.getElementById('user_email').value = btn.getAttribute('data-email') || '';
document.getElementById('user_role').value = btn.getAttribute('data-role');
document.getElementById('user_password').value = '';
document.getElementById('user_password').required = false;
document.getElementById('user_password').placeholder = 'Leave blank to keep current password';
document.getElementById('user_username').readOnly = true;
document.getElementById('user-form').setAttribute('action', '{{ url_for("main.edit_user") }}');
};
});
// Delete User button logic
Array.from(document.getElementsByClassName('delete-user-btn')).forEach(function(btn) {
btn.onclick = function() {
document.getElementById('delete-user-popup').style.display = 'flex';
document.getElementById('delete-username').innerText = btn.getAttribute('data-username');
document.getElementById('delete-user-id').value = btn.getAttribute('data-user-id');
};
});
document.getElementById('close-delete-popup-btn').onclick = function() {
document.getElementById('delete-user-popup').style.display = 'none';
};