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:
@@ -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({
|
||||
|
||||
@@ -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():
|
||||
|
||||
252
py_app/app/templates/log_explorer.html
Normal file
252
py_app/app/templates/log_explorer.html
Normal 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 %}
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user