updated control access
This commit is contained in:
90
py_app/app/access_control.py
Normal file
90
py_app/app/access_control.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Simple access control decorators for the 4-tier system
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import session, redirect, url_for, flash, request
|
||||
from .permissions_simple import check_access, ROLES
|
||||
|
||||
def requires_role(min_role_level=None, required_modules=None, page=None):
|
||||
"""
|
||||
Simple role-based access decorator
|
||||
|
||||
Args:
|
||||
min_role_level (int): Minimum role level required (50, 70, 90, 100)
|
||||
required_modules (list): Required modules for access
|
||||
page (str): Page name for automatic access checking
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Check if user is logged in
|
||||
if 'user' not in session:
|
||||
flash('Please log in to access this page.')
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
user_role = session.get('role')
|
||||
user_modules = session.get('modules', [])
|
||||
|
||||
# If page is specified, use automatic access checking
|
||||
if page:
|
||||
if not check_access(user_role, user_modules, page):
|
||||
flash('Access denied: You do not have permission to access this page.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
# Manual role level checking
|
||||
if min_role_level:
|
||||
user_level = ROLES.get(user_role, {}).get('level', 0)
|
||||
if user_level < min_role_level:
|
||||
flash('Access denied: Insufficient privileges.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
# Module requirement checking
|
||||
if required_modules:
|
||||
if user_role in ['superadmin', 'admin']:
|
||||
# Superadmin and admin have access to all modules
|
||||
pass
|
||||
else:
|
||||
if not any(module in user_modules for module in required_modules):
|
||||
flash('Access denied: You do not have access to this module.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def superadmin_only(f):
|
||||
"""Decorator for superadmin-only pages"""
|
||||
return requires_role(min_role_level=100)(f)
|
||||
|
||||
def admin_plus(f):
|
||||
"""Decorator for admin and superadmin access"""
|
||||
return requires_role(min_role_level=90)(f)
|
||||
|
||||
def manager_plus(f):
|
||||
"""Decorator for manager, admin, and superadmin access"""
|
||||
return requires_role(min_role_level=70)(f)
|
||||
|
||||
def requires_quality_module(f):
|
||||
"""Decorator for quality module access"""
|
||||
return requires_role(required_modules=['quality'])(f)
|
||||
|
||||
def requires_warehouse_module(f):
|
||||
"""Decorator for warehouse module access"""
|
||||
return requires_role(required_modules=['warehouse'])(f)
|
||||
|
||||
def requires_labels_module(f):
|
||||
"""Decorator for labels module access"""
|
||||
return requires_role(required_modules=['labels'])(f)
|
||||
|
||||
def quality_manager_plus(f):
|
||||
"""Decorator for quality module manager+ access"""
|
||||
return requires_role(min_role_level=70, required_modules=['quality'])(f)
|
||||
|
||||
def warehouse_manager_plus(f):
|
||||
"""Decorator for warehouse module manager+ access"""
|
||||
return requires_role(min_role_level=70, required_modules=['warehouse'])(f)
|
||||
|
||||
def labels_manager_plus(f):
|
||||
"""Decorator for labels module manager+ access"""
|
||||
return requires_role(min_role_level=70, required_modules=['labels'])(f)
|
||||
@@ -1,110 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
# Get the instance folder path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
# Read settings from the configuration file
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
print(f"Connecting to MariaDB:")
|
||||
print(f" Host: {settings.get('server_domain', 'N/A')}")
|
||||
print(f" Port: {settings.get('port', 'N/A')}")
|
||||
print(f" Database: {settings.get('database_name', 'N/A')}")
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Adding Email Column to Users Table ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# First, check the current table structure
|
||||
print("\n1. Checking current table structure...")
|
||||
cursor.execute("DESCRIBE users")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
has_email = False
|
||||
for column in columns:
|
||||
print(f" Column: {column[0]} ({column[1]})")
|
||||
if column[0] == 'email':
|
||||
has_email = True
|
||||
|
||||
if not has_email:
|
||||
print("\n2. Adding email column...")
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(255)")
|
||||
conn.commit()
|
||||
print(" ✓ Email column added successfully")
|
||||
else:
|
||||
print("\n2. Email column already exists")
|
||||
|
||||
# Now check and display all users
|
||||
print("\n3. Current users in database:")
|
||||
cursor.execute("SELECT id, username, role, email FROM users")
|
||||
users = cursor.fetchall()
|
||||
|
||||
if users:
|
||||
print(f" Found {len(users)} users:")
|
||||
for user in users:
|
||||
email = user[3] if user[3] else "No email"
|
||||
print(f" - ID: {user[0]}, Username: {user[1]}, Role: {user[2]}, Email: {email}")
|
||||
else:
|
||||
print(" No users found - creating test users...")
|
||||
|
||||
# Create some test users
|
||||
test_users = [
|
||||
('admin_user', 'admin123', 'admin', 'admin@company.com'),
|
||||
('manager_user', 'manager123', 'manager', 'manager@company.com'),
|
||||
('warehouse_user', 'warehouse123', 'warehouse_manager', 'warehouse@company.com'),
|
||||
('quality_user', 'quality123', 'quality_manager', 'quality@company.com')
|
||||
]
|
||||
|
||||
for username, password, role, email in test_users:
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, password, role, email)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (username, password, role, email))
|
||||
print(f" ✓ Created user: {username} ({role})")
|
||||
except mariadb.IntegrityError as e:
|
||||
print(f" ⚠ User {username} already exists: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(" ✓ Test users created successfully")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Database Update Complete ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
# Get the instance folder path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
# Read settings from the configuration file
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
print(f"Connecting to MariaDB with settings:")
|
||||
print(f" Host: {settings.get('server_domain', 'N/A')}")
|
||||
print(f" Port: {settings.get('port', 'N/A')}")
|
||||
print(f" Database: {settings.get('database_name', 'N/A')}")
|
||||
print(f" Username: {settings.get('username', 'N/A')}")
|
||||
|
||||
# Create a database connection
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Checking External MariaDB Database ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create users table if it doesn't exist
|
||||
print("\n1. Creating/verifying users table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255)
|
||||
)
|
||||
''')
|
||||
print(" ✓ Users table created/verified")
|
||||
|
||||
# Check existing users
|
||||
print("\n2. Checking existing users...")
|
||||
cursor.execute("SELECT id, username, role, email FROM users")
|
||||
users = cursor.fetchall()
|
||||
|
||||
if users:
|
||||
print(f" Found {len(users)} existing users:")
|
||||
for user in users:
|
||||
email = user[3] if user[3] else "No email"
|
||||
print(f" - ID: {user[0]}, Username: {user[1]}, Role: {user[2]}, Email: {email}")
|
||||
else:
|
||||
print(" No users found in external database")
|
||||
|
||||
# Create some test users
|
||||
print("\n3. Creating test users...")
|
||||
test_users = [
|
||||
('admin_user', 'admin123', 'admin', 'admin@company.com'),
|
||||
('manager_user', 'manager123', 'manager', 'manager@company.com'),
|
||||
('warehouse_user', 'warehouse123', 'warehouse_manager', 'warehouse@company.com'),
|
||||
('quality_user', 'quality123', 'quality_manager', 'quality@company.com')
|
||||
]
|
||||
|
||||
for username, password, role, email in test_users:
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, password, role, email)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (username, password, role, email))
|
||||
print(f" ✓ Created user: {username} ({role})")
|
||||
except mariadb.IntegrityError as e:
|
||||
print(f" ⚠ User {username} already exists: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(" ✓ Test users created successfully")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Database Check Complete ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,60 +0,0 @@
|
||||
import mariadb
|
||||
import os
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Get MariaDB connection using external_server.conf"""
|
||||
settings_file = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance/external_server.conf'))
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split('=', 1)
|
||||
settings[key] = value
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def create_external_users_table():
|
||||
"""Create users table and superadmin user in external MariaDB database"""
|
||||
try:
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create users table if not exists (MariaDB syntax)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
# Insert superadmin user if not exists
|
||||
cursor.execute('''
|
||||
INSERT IGNORE INTO users (username, password, role)
|
||||
VALUES (%s, %s, %s)
|
||||
''', ('superadmin', 'superadmin123', 'superadmin'))
|
||||
|
||||
# Check if user was created/exists
|
||||
cursor.execute("SELECT username, password, role FROM users WHERE username = %s", ('superadmin',))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
print(f"SUCCESS: Superadmin user exists in external database")
|
||||
print(f"Username: {result[0]}, Password: {result[1]}, Role: {result[2]}")
|
||||
else:
|
||||
print("ERROR: Failed to create/find superadmin user")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("External MariaDB users table setup completed.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_external_users_table()
|
||||
@@ -1,110 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database script to create the order_for_labels table
|
||||
This table will store order information for label generation
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import mariadb
|
||||
from flask import Flask
|
||||
|
||||
# Add the app directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
def get_db_connection():
|
||||
"""Get database connection using settings from external_server.conf"""
|
||||
# Go up two levels from this script to reach py_app directory, then to instance
|
||||
app_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
settings_file = os.path.join(app_root, 'instance', 'external_server.conf')
|
||||
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def create_order_for_labels_table():
|
||||
"""
|
||||
Creates the order_for_labels table with the specified structure
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# First check if table already exists
|
||||
cursor.execute("SHOW TABLES LIKE 'order_for_labels'")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
print("Table 'order_for_labels' already exists.")
|
||||
# Show current structure
|
||||
cursor.execute("DESCRIBE order_for_labels")
|
||||
columns = cursor.fetchall()
|
||||
print("\nCurrent table structure:")
|
||||
for col in columns:
|
||||
print(f" {col[0]} - {col[1]} {'NULL' if col[2] == 'YES' else 'NOT NULL'}")
|
||||
else:
|
||||
# Create the table
|
||||
create_table_sql = """
|
||||
CREATE TABLE order_for_labels (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Unique identifier',
|
||||
comanda_productie VARCHAR(15) NOT NULL COMMENT 'Production Order',
|
||||
cod_articol VARCHAR(15) COMMENT 'Article Code',
|
||||
descr_com_prod VARCHAR(50) NOT NULL COMMENT 'Production Order Description',
|
||||
cantitate INT(3) NOT NULL COMMENT 'Quantity',
|
||||
com_achiz_client VARCHAR(25) COMMENT 'Client Purchase Order',
|
||||
nr_linie_com_client INT(3) COMMENT 'Client Order Line Number',
|
||||
customer_name VARCHAR(50) COMMENT 'Customer Name',
|
||||
customer_article_number VARCHAR(25) COMMENT 'Customer Article Number',
|
||||
open_for_order VARCHAR(25) COMMENT 'Open for Order Status',
|
||||
line_number INT(3) COMMENT 'Line Number',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation timestamp',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update timestamp'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table for storing order information for label generation'
|
||||
"""
|
||||
|
||||
cursor.execute(create_table_sql)
|
||||
conn.commit()
|
||||
print("✅ Table 'order_for_labels' created successfully!")
|
||||
|
||||
# Show the created structure
|
||||
cursor.execute("DESCRIBE order_for_labels")
|
||||
columns = cursor.fetchall()
|
||||
print("\n📋 Table structure:")
|
||||
for col in columns:
|
||||
null_info = 'NULL' if col[2] == 'YES' else 'NOT NULL'
|
||||
default_info = f" DEFAULT {col[4]}" if col[4] else ""
|
||||
print(f" 📌 {col[0]:<25} {col[1]:<20} {null_info}{default_info}")
|
||||
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"❌ Database error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🏗️ Creating order_for_labels table...")
|
||||
print("="*50)
|
||||
|
||||
success = create_order_for_labels_table()
|
||||
|
||||
if success:
|
||||
print("\n✅ Database setup completed successfully!")
|
||||
else:
|
||||
print("\n❌ Database setup failed!")
|
||||
|
||||
print("="*50)
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
# Get the instance folder path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
# Read settings from the configuration file
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Creating Permission Management Tables ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. Create permissions table
|
||||
print("\n1. Creating permissions table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
permission_key VARCHAR(255) UNIQUE NOT NULL,
|
||||
page VARCHAR(100) NOT NULL,
|
||||
page_name VARCHAR(255) NOT NULL,
|
||||
section VARCHAR(100) NOT NULL,
|
||||
section_name VARCHAR(255) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
action_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
print(" ✓ Permissions table created/verified")
|
||||
|
||||
# 2. Create role_permissions table
|
||||
print("\n2. Creating role_permissions table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
permission_key VARCHAR(255) NOT NULL,
|
||||
granted BOOLEAN DEFAULT TRUE,
|
||||
granted_by VARCHAR(50),
|
||||
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_role_permission (role, permission_key),
|
||||
FOREIGN KEY (permission_key) REFERENCES permissions(permission_key) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
print(" ✓ Role permissions table created/verified")
|
||||
|
||||
# 3. Create role_hierarchy table for role management
|
||||
print("\n3. Creating role_hierarchy table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS role_hierarchy (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
level INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
print(" ✓ Role hierarchy table created/verified")
|
||||
|
||||
# 4. Create permission_audit_log table for tracking changes
|
||||
print("\n4. Creating permission_audit_log table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS permission_audit_log (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
permission_key VARCHAR(255) NOT NULL,
|
||||
action ENUM('granted', 'revoked') NOT NULL,
|
||||
changed_by VARCHAR(50) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
reason TEXT,
|
||||
ip_address VARCHAR(45)
|
||||
)
|
||||
''')
|
||||
print(" ✓ Permission audit log table created/verified")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 5. Check if we need to populate initial data
|
||||
print("\n5. Checking for existing data...")
|
||||
cursor.execute("SELECT COUNT(*) FROM permissions")
|
||||
permission_count = cursor.fetchone()[0]
|
||||
|
||||
if permission_count == 0:
|
||||
print(" No permissions found - will need to populate with default data")
|
||||
print(" Run 'populate_permissions.py' to initialize the permission system")
|
||||
else:
|
||||
print(f" Found {permission_count} existing permissions")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM role_hierarchy")
|
||||
role_count = cursor.fetchone()[0]
|
||||
|
||||
if role_count == 0:
|
||||
print(" No roles found - will need to populate with default roles")
|
||||
else:
|
||||
print(f" Found {role_count} existing roles")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Permission Database Schema Created Successfully ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,45 +0,0 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def create_roles_and_users_tables(db_path):
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
# Create users table if not exists
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
role TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
# Insert superadmin user if not exists (default password: 'admin', change after first login)
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO users (username, password, role)
|
||||
VALUES (?, ?, ?)
|
||||
''', ('superadmin', 'superadmin123', 'superadmin'))
|
||||
# Create roles table if not exists
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
access_level TEXT NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
''')
|
||||
# Insert superadmin role if not exists
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO roles (name, access_level, description)
|
||||
VALUES (?, ?, ?)
|
||||
''', ('superadmin', 'full', 'Full access to all app areas and functions'))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Default path to users.db in instance folder
|
||||
instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance'))
|
||||
if not os.path.exists(instance_folder):
|
||||
os.makedirs(instance_folder)
|
||||
db_path = os.path.join(instance_folder, 'users.db')
|
||||
create_roles_and_users_tables(db_path)
|
||||
print("Roles and users tables created. Superadmin user and role initialized.")
|
||||
@@ -1,42 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
db_config = {
|
||||
"user": "trasabilitate",
|
||||
"password": "Initial01!",
|
||||
"host": "localhost",
|
||||
"database": "trasabilitate"
|
||||
}
|
||||
|
||||
# Connect to the database
|
||||
try:
|
||||
conn = mariadb.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
print("Connected to the database successfully!")
|
||||
|
||||
# Create the scan1_orders table
|
||||
create_table_query = """
|
||||
CREATE TABLE IF NOT EXISTS scan1_orders (
|
||||
Id INT AUTO_INCREMENT PRIMARY KEY, -- Auto-incremented ID with 6 digits
|
||||
operator_code VARCHAR(4) NOT NULL, -- Operator code with 4 characters
|
||||
CP_full_code VARCHAR(15) NOT NULL UNIQUE, -- Full CP code with up to 15 characters
|
||||
OC1_code VARCHAR(4) NOT NULL, -- OC1 code with 4 characters
|
||||
OC2_code VARCHAR(4) NOT NULL, -- OC2 code with 4 characters
|
||||
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED, -- Auto-generated base code (first 10 characters of CP_full_code)
|
||||
quality_code INT(3) NOT NULL, -- Quality code with 3 digits
|
||||
date DATE NOT NULL, -- Date in format dd-mm-yyyy
|
||||
time TIME NOT NULL, -- Time in format hh:mm:ss
|
||||
approved_quantity INT DEFAULT 0, -- Auto-incremented quantity for quality_code = 000
|
||||
rejected_quantity INT DEFAULT 0 -- Auto-incremented quantity for quality_code != 000
|
||||
);
|
||||
"""
|
||||
cursor.execute(create_table_query)
|
||||
print("Table 'scan1_orders' created successfully!")
|
||||
|
||||
# Commit changes and close the connection
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error connecting to the database: {e}")
|
||||
@@ -1,41 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
# (reuse from create_scan_1db.py or update as needed)
|
||||
db_config = {
|
||||
"user": "trasabilitate",
|
||||
"password": "Initial01!",
|
||||
"host": "localhost",
|
||||
"database": "trasabilitate"
|
||||
}
|
||||
|
||||
try:
|
||||
conn = mariadb.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
print("Connected to the database successfully!")
|
||||
|
||||
# Create the scanfg_orders table (same structure as scan1_orders)
|
||||
create_table_query = """
|
||||
CREATE TABLE IF NOT EXISTS scanfg_orders (
|
||||
Id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
operator_code VARCHAR(4) NOT NULL,
|
||||
CP_full_code VARCHAR(15) NOT NULL UNIQUE,
|
||||
OC1_code VARCHAR(4) NOT NULL,
|
||||
OC2_code VARCHAR(4) NOT NULL,
|
||||
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED,
|
||||
quality_code INT(3) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
time TIME NOT NULL,
|
||||
approved_quantity INT DEFAULT 0,
|
||||
rejected_quantity INT DEFAULT 0
|
||||
);
|
||||
"""
|
||||
cursor.execute(create_table_query)
|
||||
print("Table 'scanfg_orders' created successfully!")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error connecting to the database: {e}")
|
||||
@@ -1,70 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
db_config = {
|
||||
"user": "trasabilitate",
|
||||
"password": "Initial01!",
|
||||
"host": "localhost",
|
||||
"database": "trasabilitate"
|
||||
}
|
||||
|
||||
# Connect to the database
|
||||
try:
|
||||
conn = mariadb.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
print("Connected to the database successfully!")
|
||||
|
||||
# Delete old triggers if they exist
|
||||
try:
|
||||
cursor.execute("DROP TRIGGER IF EXISTS increment_approved_quantity;")
|
||||
print("Old trigger 'increment_approved_quantity' deleted successfully.")
|
||||
except mariadb.Error as e:
|
||||
print(f"Error deleting old trigger 'increment_approved_quantity': {e}")
|
||||
|
||||
try:
|
||||
cursor.execute("DROP TRIGGER IF EXISTS increment_rejected_quantity;")
|
||||
print("Old trigger 'increment_rejected_quantity' deleted successfully.")
|
||||
except mariadb.Error as e:
|
||||
print(f"Error deleting old trigger 'increment_rejected_quantity': {e}")
|
||||
|
||||
# Create corrected trigger for approved_quantity
|
||||
create_approved_trigger = """
|
||||
CREATE TRIGGER increment_approved_quantity
|
||||
BEFORE INSERT ON scan1_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.quality_code = 000 THEN
|
||||
SET NEW.approved_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scan1_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code = 000
|
||||
) + 1;
|
||||
SET NEW.rejected_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scan1_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code != 000
|
||||
);
|
||||
ELSE
|
||||
SET NEW.approved_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scan1_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code = 000
|
||||
);
|
||||
SET NEW.rejected_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scan1_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code != 000
|
||||
) + 1;
|
||||
END IF;
|
||||
END;
|
||||
"""
|
||||
cursor.execute(create_approved_trigger)
|
||||
print("Trigger 'increment_approved_quantity' created successfully!")
|
||||
|
||||
# Commit changes and close the connection
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error connecting to the database or creating triggers: {e}")
|
||||
@@ -1,73 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
db_config = {
|
||||
"user": "trasabilitate",
|
||||
"password": "Initial01!",
|
||||
"host": "localhost",
|
||||
"database": "trasabilitate"
|
||||
}
|
||||
|
||||
# Connect to the database
|
||||
try:
|
||||
conn = mariadb.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
print("Connected to the database successfully!")
|
||||
|
||||
# Delete old triggers if they exist
|
||||
try:
|
||||
cursor.execute("DROP TRIGGER IF EXISTS increment_approved_quantity_fg;")
|
||||
print("Old trigger 'increment_approved_quantity_fg' deleted successfully.")
|
||||
except mariadb.Error as e:
|
||||
print(f"Error deleting old trigger 'increment_approved_quantity_fg': {e}")
|
||||
|
||||
try:
|
||||
cursor.execute("DROP TRIGGER IF EXISTS increment_rejected_quantity_fg;")
|
||||
print("Old trigger 'increment_rejected_quantity_fg' deleted successfully.")
|
||||
except mariadb.Error as e:
|
||||
print(f"Error deleting old trigger 'increment_rejected_quantity_fg': {e}")
|
||||
|
||||
# Create corrected trigger for approved_quantity in scanfg_orders
|
||||
create_approved_trigger_fg = """
|
||||
CREATE TRIGGER increment_approved_quantity_fg
|
||||
BEFORE INSERT ON scanfg_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.quality_code = 000 THEN
|
||||
SET NEW.approved_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scanfg_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code = 000
|
||||
) + 1;
|
||||
SET NEW.rejected_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scanfg_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code != 000
|
||||
);
|
||||
ELSE
|
||||
SET NEW.approved_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scanfg_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code = 000
|
||||
);
|
||||
SET NEW.rejected_quantity = (
|
||||
SELECT COUNT(*)
|
||||
FROM scanfg_orders
|
||||
WHERE CP_base_code = NEW.CP_base_code AND quality_code != 000
|
||||
) + 1;
|
||||
END IF;
|
||||
END;
|
||||
"""
|
||||
cursor.execute(create_approved_trigger_fg)
|
||||
print("Trigger 'increment_approved_quantity_fg' created successfully for scanfg_orders table!")
|
||||
|
||||
# Commit changes and close the connection
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print("\n✅ All triggers for scanfg_orders table created successfully!")
|
||||
print("The approved_quantity and rejected_quantity will now be calculated automatically.")
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error connecting to the database or creating triggers: {e}")
|
||||
@@ -1,25 +0,0 @@
|
||||
import mariadb
|
||||
from app.warehouse import get_db_connection
|
||||
from flask import Flask
|
||||
import os
|
||||
|
||||
def create_warehouse_locations_table():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS warehouse_locations (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
location_code VARCHAR(12) NOT NULL UNIQUE,
|
||||
size INT,
|
||||
description VARCHAR(250)
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
instance_path = os.path.abspath("instance")
|
||||
app = Flask(__name__, instance_path=instance_path)
|
||||
with app.app_context():
|
||||
create_warehouse_locations_table()
|
||||
print("warehouse_locations table created or already exists.")
|
||||
@@ -1,30 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
def get_db_connection():
|
||||
return mariadb.connect(
|
||||
user="trasabilitate", # Replace with your username
|
||||
password="Initial01!", # Replace with your password
|
||||
host="localhost", # Replace with your host
|
||||
port=3306, # Default MariaDB port
|
||||
database="trasabilitate_database" # Replace with your database name
|
||||
)
|
||||
|
||||
try:
|
||||
# Connect to the database
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Delete query
|
||||
delete_query = "DELETE FROM scan1_orders"
|
||||
cursor.execute(delete_query)
|
||||
conn.commit()
|
||||
|
||||
print("All data from the 'scan1_orders' table has been deleted successfully.")
|
||||
|
||||
# Close the connection
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error deleting data: {e}")
|
||||
@@ -1,26 +0,0 @@
|
||||
import mariadb
|
||||
import os
|
||||
|
||||
def get_external_db_connection():
|
||||
settings_file = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance/external_server.conf'))
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split('=', 1)
|
||||
settings[key] = value
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DROP TABLE IF EXISTS users")
|
||||
cursor.execute("DROP TABLE IF EXISTS roles")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Dropped users and roles tables from external database.")
|
||||
@@ -1,53 +0,0 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def check_database(db_path, description):
|
||||
"""Check if a database exists and show its users."""
|
||||
if os.path.exists(db_path):
|
||||
print(f"\n{description}: FOUND at {db_path}")
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if users table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
|
||||
if cursor.fetchone():
|
||||
cursor.execute("SELECT id, username, password, role FROM users")
|
||||
users = cursor.fetchall()
|
||||
if users:
|
||||
print("Users in this database:")
|
||||
for user in users:
|
||||
print(f" ID: {user[0]}, Username: {user[1]}, Password: {user[2]}, Role: {user[3]}")
|
||||
else:
|
||||
print(" Users table exists but is empty")
|
||||
else:
|
||||
print(" No users table found")
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f" Error reading database: {e}")
|
||||
else:
|
||||
print(f"\n{description}: NOT FOUND at {db_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check different possible locations for users.db
|
||||
|
||||
# 1. Root quality_recticel/instance/users.db
|
||||
root_instance = "/home/ske087/quality_recticel/instance/users.db"
|
||||
check_database(root_instance, "Root instance users.db")
|
||||
|
||||
# 2. App instance folder
|
||||
app_instance = "/home/ske087/quality_recticel/py_app/instance/users.db"
|
||||
check_database(app_instance, "App instance users.db")
|
||||
|
||||
# 3. Current working directory
|
||||
cwd_db = "/home/ske087/quality_recticel/py_app/users.db"
|
||||
check_database(cwd_db, "Working directory users.db")
|
||||
|
||||
# 4. Flask app database (relative to py_app)
|
||||
flask_db = "/home/ske087/quality_recticel/py_app/app/users.db"
|
||||
check_database(flask_db, "Flask app users.db")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("RECOMMENDATION:")
|
||||
print("The login should use the external MariaDB database.")
|
||||
print("Make sure you have created the superadmin user in MariaDB using create_roles_table.py")
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the app directory to the path so we can import our permissions module
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Populating Permission System ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. Populate all permissions
|
||||
print("\n1. Populating permissions...")
|
||||
permissions = get_all_permissions()
|
||||
|
||||
for perm in permissions:
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO permissions (permission_key, page, page_name, section, section_name, action, action_name)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
page_name = VALUES(page_name),
|
||||
section_name = VALUES(section_name),
|
||||
action_name = VALUES(action_name),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (
|
||||
perm['key'],
|
||||
perm['page'],
|
||||
perm['page_name'],
|
||||
perm['section'],
|
||||
perm['section_name'],
|
||||
perm['action'],
|
||||
perm['action_name']
|
||||
))
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error inserting permission {perm['key']}: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(f" ✓ Populated {len(permissions)} permissions")
|
||||
|
||||
# 2. Populate role hierarchy
|
||||
print("\n2. Populating role hierarchy...")
|
||||
for role_name, role_data in ROLE_HIERARCHY.items():
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO role_hierarchy (role_name, display_name, description, level)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name),
|
||||
description = VALUES(description),
|
||||
level = VALUES(level),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (
|
||||
role_name,
|
||||
role_data['name'],
|
||||
role_data['description'],
|
||||
role_data['level']
|
||||
))
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error inserting role {role_name}: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(f" ✓ Populated {len(ROLE_HIERARCHY)} roles")
|
||||
|
||||
# 3. Set default permissions for each role
|
||||
print("\n3. Setting default role permissions...")
|
||||
for role_name in ROLE_HIERARCHY.keys():
|
||||
default_permissions = get_default_permissions_for_role(role_name)
|
||||
|
||||
print(f" Setting permissions for {role_name}: {len(default_permissions)} permissions")
|
||||
|
||||
for permission_key in default_permissions:
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO role_permissions (role, permission_key, granted, granted_by)
|
||||
VALUES (%s, %s, TRUE, 'system')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
granted = TRUE,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (role_name, permission_key))
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error setting permission {permission_key} for {role_name}: {e}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 4. Show summary
|
||||
print("\n4. Permission Summary:")
|
||||
cursor.execute('''
|
||||
SELECT r.role_name, r.display_name, COUNT(rp.permission_key) as permission_count
|
||||
FROM role_hierarchy r
|
||||
LEFT JOIN role_permissions rp ON r.role_name = rp.role AND rp.granted = TRUE
|
||||
GROUP BY r.role_name, r.display_name
|
||||
ORDER BY r.level DESC
|
||||
''')
|
||||
|
||||
results = cursor.fetchall()
|
||||
for role_name, display_name, count in results:
|
||||
print(f" {display_name} ({role_name}): {count} permissions")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Permission System Initialization Complete ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,30 +0,0 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance'))
|
||||
db_path = os.path.join(instance_folder, 'users.db')
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print("users.db not found at", db_path)
|
||||
exit(1)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if users table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
|
||||
if not cursor.fetchone():
|
||||
print("No users table found in users.db.")
|
||||
conn.close()
|
||||
exit(1)
|
||||
|
||||
# Print all users
|
||||
cursor.execute("SELECT id, username, password, role FROM users")
|
||||
rows = cursor.fetchall()
|
||||
if not rows:
|
||||
print("No users found in users.db.")
|
||||
else:
|
||||
print("Users in users.db:")
|
||||
for row in rows:
|
||||
print(f"id={row[0]}, username={row[1]}, password={row[2]}, role={row[3]}")
|
||||
conn.close()
|
||||
@@ -1,34 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
db_config = {
|
||||
"user": "trasabilitate",
|
||||
"password": "Initial01!",
|
||||
"host": "localhost",
|
||||
"database": "trasabilitate_database"
|
||||
}
|
||||
|
||||
try:
|
||||
# Connect to the database
|
||||
conn = mariadb.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query to fetch all records from the scan1 table
|
||||
query = "SELECT * FROM scan1_orders ORDER BY Id DESC LIMIT 15"
|
||||
cursor.execute(query)
|
||||
|
||||
# Fetch and print the results
|
||||
rows = cursor.fetchall()
|
||||
if rows:
|
||||
print("Records in the 'scan1_orders' table:")
|
||||
for row in rows:
|
||||
print(row)
|
||||
else:
|
||||
print("No records found in the 'scan1_orders' table.")
|
||||
|
||||
# Close the connection
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error connecting to the database: {e}")
|
||||
@@ -1,50 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
DB_CONFIG = {
|
||||
"user": "sa",
|
||||
"password": "12345678",
|
||||
"host": "localhost",
|
||||
"database": "recticel"
|
||||
}
|
||||
|
||||
def recreate_order_for_labels_table():
|
||||
conn = mariadb.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
print("Connected to the database successfully!")
|
||||
|
||||
# Drop the table if it exists
|
||||
cursor.execute("DROP TABLE IF EXISTS order_for_labels")
|
||||
print("Dropped existing 'order_for_labels' table.")
|
||||
|
||||
# Create the table with the new unique constraint
|
||||
create_table_sql = """
|
||||
CREATE TABLE order_for_labels (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Unique identifier',
|
||||
comanda_productie VARCHAR(15) NOT NULL UNIQUE COMMENT 'Production Order (unique)',
|
||||
cod_articol VARCHAR(15) COMMENT 'Article Code',
|
||||
descr_com_prod VARCHAR(50) NOT NULL COMMENT 'Production Order Description',
|
||||
cantitate INT(3) NOT NULL COMMENT 'Quantity',
|
||||
data_livrare DATE COMMENT 'Delivery date',
|
||||
dimensiune VARCHAR(20) COMMENT 'Dimensions',
|
||||
com_achiz_client VARCHAR(25) COMMENT 'Client Purchase Order',
|
||||
nr_linie_com_client INT(3) COMMENT 'Client Order Line Number',
|
||||
customer_name VARCHAR(50) COMMENT 'Customer Name',
|
||||
customer_article_number VARCHAR(25) COMMENT 'Customer Article Number',
|
||||
open_for_order VARCHAR(25) COMMENT 'Open for Order Status',
|
||||
line_number INT(3) COMMENT 'Line Number',
|
||||
printed_labels TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Boolean flag: 0=labels not printed, 1=labels printed',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation timestamp',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update timestamp'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table for storing order information for label generation';
|
||||
"""
|
||||
cursor.execute(create_table_sql)
|
||||
print("Created new 'order_for_labels' table with unique comanda_productie.")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
recreate_order_for_labels_table()
|
||||
@@ -1,34 +0,0 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'your_secret_key' # Use the same key as in __init__.py
|
||||
|
||||
instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance'))
|
||||
if not os.path.exists(instance_folder):
|
||||
os.makedirs(instance_folder)
|
||||
db_path = os.path.join(instance_folder, 'users.db')
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create users table if not exists
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
role TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
# Insert superadmin user if not exists
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO users (username, password, role)
|
||||
VALUES (?, ?, ?)
|
||||
''', ('superadmin', 'superadmin123', 'superadmin'))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Internal users.db seeded with superadmin user.")
|
||||
@@ -1,37 +0,0 @@
|
||||
import mariadb
|
||||
|
||||
# Database connection credentials
|
||||
def get_db_connection():
|
||||
return mariadb.connect(
|
||||
user="trasabilitate", # Replace with your username
|
||||
password="Initial01!", # Replace with your password
|
||||
host="localhost", # Replace with your host
|
||||
port=3306, # Default MariaDB port
|
||||
database="trasabilitate_database" # Replace with your database name
|
||||
)
|
||||
|
||||
try:
|
||||
# Connect to the database
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Insert query
|
||||
insert_query = """
|
||||
INSERT INTO scan1_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
# Values to insert
|
||||
values = ('OP01', 'CP12345678-0002', 'OC11', 'OC22', 000, '2025-04-22', '14:30:00')
|
||||
|
||||
# Execute the query
|
||||
cursor.execute(insert_query, values)
|
||||
conn.commit()
|
||||
|
||||
print("Test data inserted successfully into scan1_orders.")
|
||||
|
||||
# Close the connection
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error inserting data: {e}")
|
||||
226
py_app/app/permissions_simple.py
Normal file
226
py_app/app/permissions_simple.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Simplified 4-Tier Role-Based Access Control System
|
||||
Clear hierarchy: Superadmin → Admin → Manager → Worker
|
||||
Module-based permissions: Quality, Labels, Warehouse
|
||||
"""
|
||||
|
||||
# APPLICATION MODULES
|
||||
MODULES = {
|
||||
'quality': {
|
||||
'name': 'Quality Control',
|
||||
'scan_pages': ['quality', 'fg_quality'],
|
||||
'management_pages': ['quality_reports', 'quality_settings'],
|
||||
'worker_access': ['scan_only'] # Workers can only scan, no reports
|
||||
},
|
||||
'labels': {
|
||||
'name': 'Label Management',
|
||||
'scan_pages': ['label_scan'],
|
||||
'management_pages': ['label_creation', 'label_reports'],
|
||||
'worker_access': ['scan_only']
|
||||
},
|
||||
'warehouse': {
|
||||
'name': 'Warehouse Management',
|
||||
'scan_pages': ['move_orders'],
|
||||
'management_pages': ['create_locations', 'warehouse_reports', 'inventory_management'],
|
||||
'worker_access': ['move_orders_only'] # Workers can move orders but not create locations
|
||||
}
|
||||
}
|
||||
|
||||
# 4-TIER ROLE STRUCTURE
|
||||
ROLES = {
|
||||
'superadmin': {
|
||||
'name': 'Super Administrator',
|
||||
'level': 100,
|
||||
'description': 'Full system access - complete control over all modules and system settings',
|
||||
'access': {
|
||||
'all_modules': True,
|
||||
'all_pages': True,
|
||||
'restricted_pages': [] # No restrictions
|
||||
}
|
||||
},
|
||||
'admin': {
|
||||
'name': 'Administrator',
|
||||
'level': 90,
|
||||
'description': 'Full app access except role permissions and extension download',
|
||||
'access': {
|
||||
'all_modules': True,
|
||||
'all_pages': True,
|
||||
'restricted_pages': ['role_permissions', 'download_extension']
|
||||
}
|
||||
},
|
||||
'manager': {
|
||||
'name': 'Manager',
|
||||
'level': 70,
|
||||
'description': 'Complete module access - can manage one or more modules (quality/labels/warehouse)',
|
||||
'access': {
|
||||
'all_modules': False, # Only assigned modules
|
||||
'module_access': 'full', # Full access to assigned modules
|
||||
'can_cumulate': True, # Can have multiple modules
|
||||
'restricted_pages': ['role_permissions', 'download_extension', 'system_settings']
|
||||
}
|
||||
},
|
||||
'worker': {
|
||||
'name': 'Worker',
|
||||
'level': 50,
|
||||
'description': 'Limited module access - can perform basic operations in assigned modules',
|
||||
'access': {
|
||||
'all_modules': False, # Only assigned modules
|
||||
'module_access': 'limited', # Limited access (scan pages only)
|
||||
'can_cumulate': True, # Can have multiple modules
|
||||
'restricted_pages': ['role_permissions', 'download_extension', 'system_settings', 'reports']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# PAGE ACCESS RULES
|
||||
PAGE_ACCESS = {
|
||||
# System pages accessible by role level
|
||||
'dashboard': {'min_level': 50, 'modules': []},
|
||||
'settings': {'min_level': 90, 'modules': []},
|
||||
'role_permissions': {'min_level': 100, 'modules': []}, # Superadmin only
|
||||
'download_extension': {'min_level': 100, 'modules': []}, # Superadmin only
|
||||
|
||||
# Quality module pages
|
||||
'quality': {'min_level': 50, 'modules': ['quality']},
|
||||
'fg_quality': {'min_level': 50, 'modules': ['quality']},
|
||||
'quality_reports': {'min_level': 70, 'modules': ['quality']}, # Manager+ only
|
||||
'reports': {'min_level': 70, 'modules': ['quality']}, # Manager+ only for quality reports
|
||||
|
||||
# Warehouse module pages
|
||||
'warehouse': {'min_level': 50, 'modules': ['warehouse']},
|
||||
'move_orders': {'min_level': 50, 'modules': ['warehouse']},
|
||||
'create_locations': {'min_level': 70, 'modules': ['warehouse']}, # Manager+ only
|
||||
'warehouse_reports': {'min_level': 70, 'modules': ['warehouse']}, # Manager+ only
|
||||
|
||||
# Labels module pages
|
||||
'labels': {'min_level': 50, 'modules': ['labels']},
|
||||
'label_scan': {'min_level': 50, 'modules': ['labels']},
|
||||
'label_creation': {'min_level': 70, 'modules': ['labels']}, # Manager+ only
|
||||
'label_reports': {'min_level': 70, 'modules': ['labels']} # Manager+ only
|
||||
}
|
||||
|
||||
def check_access(user_role, user_modules, page):
|
||||
"""
|
||||
Simple access check for the 4-tier system
|
||||
|
||||
Args:
|
||||
user_role (str): User's role (superadmin, admin, manager, worker)
|
||||
user_modules (list): User's assigned modules ['quality', 'warehouse']
|
||||
page (str): Page being accessed
|
||||
|
||||
Returns:
|
||||
bool: True if access granted, False otherwise
|
||||
"""
|
||||
if user_role not in ROLES:
|
||||
return False
|
||||
|
||||
user_level = ROLES[user_role]['level']
|
||||
|
||||
# Check if page exists in our access rules
|
||||
if page not in PAGE_ACCESS:
|
||||
return False
|
||||
|
||||
page_config = PAGE_ACCESS[page]
|
||||
|
||||
# Check minimum level requirement
|
||||
if user_level < page_config['min_level']:
|
||||
return False
|
||||
|
||||
# Check restricted pages for this role
|
||||
if page in ROLES[user_role]['access']['restricted_pages']:
|
||||
return False
|
||||
|
||||
# Check module requirements
|
||||
required_modules = page_config['modules']
|
||||
if required_modules:
|
||||
# Page requires specific modules
|
||||
# Superadmin and admin have access to all modules by default
|
||||
if ROLES[user_role]['access']['all_modules']:
|
||||
return True
|
||||
# Other roles need to have the required module assigned
|
||||
if not any(module in user_modules for module in required_modules):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_user_accessible_pages(user_role, user_modules):
|
||||
"""
|
||||
Get list of pages accessible to a user
|
||||
|
||||
Args:
|
||||
user_role (str): User's role
|
||||
user_modules (list): User's assigned modules
|
||||
|
||||
Returns:
|
||||
list: List of accessible page names
|
||||
"""
|
||||
accessible_pages = []
|
||||
|
||||
for page in PAGE_ACCESS.keys():
|
||||
if check_access(user_role, user_modules, page):
|
||||
accessible_pages.append(page)
|
||||
|
||||
return accessible_pages
|
||||
|
||||
def validate_user_modules(user_role, user_modules):
|
||||
"""
|
||||
Validate that user's module assignment is valid for their role
|
||||
|
||||
Args:
|
||||
user_role (str): User's role
|
||||
user_modules (list): User's assigned modules
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, error_message)
|
||||
"""
|
||||
if user_role not in ROLES:
|
||||
return False, "Invalid role"
|
||||
|
||||
role_config = ROLES[user_role]
|
||||
|
||||
# Superadmin and admin have access to all modules by default
|
||||
if role_config['access']['all_modules']:
|
||||
return True, ""
|
||||
|
||||
# Manager can have multiple modules
|
||||
if user_role == 'manager':
|
||||
if not user_modules:
|
||||
return False, "Managers must have at least one module assigned"
|
||||
valid_modules = list(MODULES.keys())
|
||||
for module in user_modules:
|
||||
if module not in valid_modules:
|
||||
return False, f"Invalid module: {module}"
|
||||
return True, ""
|
||||
|
||||
# Worker can have multiple modules now
|
||||
if user_role == 'worker':
|
||||
if not user_modules:
|
||||
return False, "Workers must have at least one module assigned"
|
||||
valid_modules = list(MODULES.keys())
|
||||
for module in user_modules:
|
||||
if module not in valid_modules:
|
||||
return False, f"Invalid module: {module}"
|
||||
return True, ""
|
||||
|
||||
return True, ""
|
||||
|
||||
def get_role_description(role):
|
||||
"""Get human-readable role description"""
|
||||
return ROLES.get(role, {}).get('description', 'Unknown role')
|
||||
|
||||
def get_available_modules():
|
||||
"""Get list of available modules"""
|
||||
return list(MODULES.keys())
|
||||
|
||||
def can_access_reports(user_role, user_modules, module):
|
||||
"""
|
||||
Check if user can access reports for a specific module
|
||||
Worker level users cannot access reports
|
||||
"""
|
||||
if user_role == 'worker':
|
||||
return False
|
||||
|
||||
if module in user_modules or ROLES[user_role]['access']['all_modules']:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -32,10 +32,8 @@ bp = Blueprint('main', __name__)
|
||||
warehouse_bp = Blueprint('warehouse', __name__)
|
||||
|
||||
@bp.route('/main_scan')
|
||||
@requires_quality_module
|
||||
def main_scan():
|
||||
if 'role' not in session or session['role'] not in ['superadmin', 'admin', 'administrator', 'scan']:
|
||||
flash('Access denied: Scan users only.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return render_template('main_page_scan.html')
|
||||
|
||||
@bp.route('/', methods=['GET', 'POST'])
|
||||
@@ -386,6 +384,58 @@ def delete_user_simple():
|
||||
flash('Error deleting user.')
|
||||
return redirect(url_for('main.user_management_simple'))
|
||||
|
||||
@bp.route('/quick_update_modules', methods=['POST'])
|
||||
@admin_plus
|
||||
def quick_update_modules():
|
||||
"""Quick update of user modules without changing other details"""
|
||||
try:
|
||||
user_id = request.form.get('user_id')
|
||||
modules = request.form.getlist('modules')
|
||||
|
||||
if not user_id:
|
||||
flash('User ID is required.')
|
||||
return redirect(url_for('main.user_management_simple'))
|
||||
|
||||
# Get current user to validate role
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT username, role 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 = 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)
|
||||
|
||||
# Update modules only
|
||||
cursor.execute("UPDATE users SET modules=%s WHERE id=%s", (modules_json, user_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
flash(f'Modules updated successfully for user "{username}".')
|
||||
return redirect(url_for('main.user_management_simple'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating modules: {e}")
|
||||
flash('Error updating modules.')
|
||||
return redirect(url_for('main.user_management_simple'))
|
||||
|
||||
@bp.route('/reports')
|
||||
@requires_quality_module
|
||||
def reports():
|
||||
@@ -499,10 +549,8 @@ def logout():
|
||||
|
||||
# Finish Goods Scan Route
|
||||
@bp.route('/fg_scan', methods=['GET', 'POST'])
|
||||
@requires_quality_module
|
||||
def fg_scan():
|
||||
if 'role' not in session or session['role'] not in ['superadmin', 'administrator', 'admin', 'scan']:
|
||||
flash('Access denied: Scan users only.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
# Handle form submission
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
# Quality Recticel Windows Print Service - Installation Guide
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
The Quality Recticel Windows Print Service enables **silent PDF printing** directly from the web application through a Chrome extension. This system eliminates the need for manual PDF downloads and provides seamless label printing functionality.
|
||||
|
||||
## 🏗️ System Architecture
|
||||
|
||||
```
|
||||
Web Application (print_module.html)
|
||||
↓
|
||||
Windows Print Service (localhost:8765)
|
||||
↓
|
||||
Chrome Extension (Native Messaging)
|
||||
↓
|
||||
Windows Print System
|
||||
```
|
||||
|
||||
## 📦 Package Contents
|
||||
|
||||
```
|
||||
windows_print_service/
|
||||
├── print_service.py # Main Windows service (Flask API)
|
||||
├── service_manager.py # Service installation & management
|
||||
├── install_service.bat # Automated installation script
|
||||
├── chrome_extension/ # Chrome extension files
|
||||
│ ├── manifest.json # Extension configuration
|
||||
│ ├── background.js # Service worker
|
||||
│ ├── content.js # Page integration
|
||||
│ ├── popup.html # Extension UI
|
||||
│ ├── popup.js # Extension logic
|
||||
│ └── icons/ # Extension icons
|
||||
└── INSTALLATION_GUIDE.md # This documentation
|
||||
```
|
||||
|
||||
## 🔧 Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **Operating System**: Windows 10/11 (64-bit)
|
||||
- **Python**: Python 3.8 or higher
|
||||
- **Browser**: Google Chrome (latest version)
|
||||
- **Privileges**: Administrator access required for installation
|
||||
|
||||
### Python Dependencies
|
||||
The following packages will be installed automatically:
|
||||
- `flask` - Web service framework
|
||||
- `flask-cors` - Cross-origin resource sharing
|
||||
- `requests` - HTTP client library
|
||||
- `pywin32` - Windows service integration
|
||||
|
||||
## 🚀 Installation Process
|
||||
|
||||
### Step 1: Download and Extract Files
|
||||
|
||||
1. Download the `windows_print_service` folder to your system
|
||||
2. Extract to a permanent location (e.g., `C:\QualityRecticel\PrintService\`)
|
||||
3. **Do not move or delete this folder after installation**
|
||||
|
||||
### Step 2: Install Windows Service
|
||||
|
||||
#### Method A: Automated Installation (Recommended)
|
||||
|
||||
1. **Right-click** on `install_service.bat`
|
||||
2. Select **"Run as administrator"**
|
||||
3. Click **"Yes"** when Windows UAC prompt appears
|
||||
4. Wait for installation to complete
|
||||
|
||||
#### Method B: Manual Installation
|
||||
|
||||
If the automated script fails, follow these steps:
|
||||
|
||||
```bash
|
||||
# Open Command Prompt as Administrator
|
||||
cd C:\path\to\windows_print_service
|
||||
|
||||
# Install Python dependencies
|
||||
pip install flask flask-cors requests pywin32
|
||||
|
||||
# Install Windows service
|
||||
python service_manager.py install
|
||||
|
||||
# Add firewall exception
|
||||
netsh advfirewall firewall add rule name="Quality Recticel Print Service" dir=in action=allow protocol=TCP localport=8765
|
||||
|
||||
# Create Chrome extension registry entry
|
||||
reg add "HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\com.qualityrecticel.printservice" /ve /d "%cd%\chrome_extension\manifest.json" /f
|
||||
```
|
||||
|
||||
### Step 3: Install Chrome Extension
|
||||
|
||||
1. Open **Google Chrome**
|
||||
2. Navigate to `chrome://extensions/`
|
||||
3. Enable **"Developer mode"** (toggle in top-right corner)
|
||||
4. Click **"Load unpacked"**
|
||||
5. Select the `chrome_extension` folder
|
||||
6. Verify the extension appears with a printer icon
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
#### Check Windows Service Status
|
||||
|
||||
1. Press `Win + R`, type `services.msc`, press Enter
|
||||
2. Look for **"Quality Recticel Print Service"**
|
||||
3. Status should show **"Running"**
|
||||
4. Startup type should be **"Automatic"**
|
||||
|
||||
#### Test API Endpoints
|
||||
|
||||
Open a web browser and visit:
|
||||
- **Health Check**: `http://localhost:8765/health`
|
||||
- **Printer List**: `http://localhost:8765/printers`
|
||||
|
||||
Expected response for health check:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"service": "Quality Recticel Print Service",
|
||||
"version": "1.0",
|
||||
"timestamp": "2025-09-21T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### Test Chrome Extension
|
||||
|
||||
1. Click the extension icon in Chrome toolbar
|
||||
2. Verify it shows "Service Status: Connected ✅"
|
||||
3. Check that printers are listed
|
||||
4. Try the "Test Print" button
|
||||
|
||||
## 🔄 Web Application Integration
|
||||
|
||||
The web application automatically detects the Windows service and adapts the user interface:
|
||||
|
||||
### Service Available (Green Button)
|
||||
- Button text: **"🖨️ Print Labels (Silent)"**
|
||||
- Functionality: Direct printing to default printer
|
||||
- User experience: Click → Labels print immediately
|
||||
|
||||
### Service Unavailable (Blue Button)
|
||||
- Button text: **"📄 Generate PDF"**
|
||||
- Functionality: PDF download for manual printing
|
||||
- User experience: Click → PDF downloads to browser
|
||||
|
||||
### Detection Logic
|
||||
```javascript
|
||||
// Automatic service detection on page load
|
||||
const response = await fetch('http://localhost:8765/health');
|
||||
if (response.ok) {
|
||||
// Service available - enable silent printing
|
||||
} else {
|
||||
// Service unavailable - fallback to PDF download
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Configuration
|
||||
|
||||
### Service Configuration
|
||||
|
||||
The service runs with the following default settings:
|
||||
|
||||
| Setting | Value | Description |
|
||||
|---------|-------|-------------|
|
||||
| **Port** | 8765 | Local API port |
|
||||
| **Host** | localhost | Service binding |
|
||||
| **Startup** | Automatic | Starts with Windows |
|
||||
| **Printer** | Default | Uses system default printer |
|
||||
| **Copies** | 1 | Default print copies |
|
||||
|
||||
### Chrome Extension Permissions
|
||||
|
||||
The extension requires these permissions:
|
||||
- `printing` - Access to printer functionality
|
||||
- `nativeMessaging` - Communication with Windows service
|
||||
- `activeTab` - Access to current webpage
|
||||
- `storage` - Save extension settings
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Service Not Starting
|
||||
**Symptoms**: API not accessible at localhost:8765
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Check service status
|
||||
python -c "from service_manager import service_status; service_status()"
|
||||
|
||||
# Restart service manually
|
||||
python service_manager.py restart
|
||||
|
||||
# Check Windows Event Viewer for service errors
|
||||
```
|
||||
|
||||
#### 2. Chrome Extension Not Working
|
||||
**Symptoms**: Extension shows "Service Status: Disconnected ❌"
|
||||
**Solutions**:
|
||||
- Verify Windows service is running
|
||||
- Check firewall settings (port 8765 must be open)
|
||||
- Reload the Chrome extension
|
||||
- Restart Chrome browser
|
||||
|
||||
#### 3. Firewall Blocking Connection
|
||||
**Symptoms**: Service runs but web page can't connect
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Add firewall rule manually
|
||||
netsh advfirewall firewall add rule name="Quality Recticel Print Service" dir=in action=allow protocol=TCP localport=8765
|
||||
|
||||
# Or disable Windows Firewall temporarily to test
|
||||
```
|
||||
|
||||
#### 4. Permission Denied Errors
|
||||
**Symptoms**: Installation fails with permission errors
|
||||
**Solutions**:
|
||||
- Ensure running as Administrator
|
||||
- Check Windows UAC settings
|
||||
- Verify Python installation permissions
|
||||
|
||||
#### 5. Print Jobs Not Processing
|
||||
**Symptoms**: API accepts requests but nothing prints
|
||||
**Solutions**:
|
||||
- Check default printer configuration
|
||||
- Verify printer drivers are installed
|
||||
- Test manual printing from other applications
|
||||
- Check Windows Print Spooler service
|
||||
|
||||
### Log Files
|
||||
|
||||
Check these locations for troubleshooting:
|
||||
|
||||
| Component | Log Location |
|
||||
|-----------|--------------|
|
||||
| **Windows Service** | `print_service.log` (same folder as service) |
|
||||
| **Chrome Extension** | Chrome DevTools → Extensions → Background page |
|
||||
| **Windows Event Log** | Event Viewer → Windows Logs → System |
|
||||
|
||||
### Diagnostic Commands
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
python service_manager.py status
|
||||
|
||||
# Test API manually
|
||||
curl http://localhost:8765/health
|
||||
|
||||
# List available printers
|
||||
curl http://localhost:8765/printers
|
||||
|
||||
# Check Windows service
|
||||
sc query QualityRecticelPrintService
|
||||
|
||||
# Check listening ports
|
||||
netstat -an | findstr :8765
|
||||
```
|
||||
|
||||
## 🔄 Maintenance
|
||||
|
||||
### Updating the Service
|
||||
|
||||
1. Stop the current service:
|
||||
```bash
|
||||
python service_manager.py stop
|
||||
```
|
||||
|
||||
2. Replace service files with new versions
|
||||
|
||||
3. Restart the service:
|
||||
```bash
|
||||
python service_manager.py start
|
||||
```
|
||||
|
||||
### Uninstalling
|
||||
|
||||
#### Remove Chrome Extension
|
||||
1. Go to `chrome://extensions/`
|
||||
2. Find "Quality Recticel Print Service"
|
||||
3. Click "Remove"
|
||||
|
||||
#### Remove Windows Service
|
||||
```bash
|
||||
# Run as Administrator
|
||||
python service_manager.py uninstall
|
||||
```
|
||||
|
||||
#### Remove Firewall Rule
|
||||
```bash
|
||||
netsh advfirewall firewall delete rule name="Quality Recticel Print Service"
|
||||
```
|
||||
|
||||
## 📞 Support Information
|
||||
|
||||
### API Endpoints Reference
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/health` | GET | Service health check |
|
||||
| `/printers` | GET | List available printers |
|
||||
| `/print/pdf` | POST | Print PDF from URL |
|
||||
| `/print/silent` | POST | Silent print with metadata |
|
||||
|
||||
### Request Examples
|
||||
|
||||
**Silent Print Request**:
|
||||
```json
|
||||
POST /print/silent
|
||||
{
|
||||
"pdf_url": "http://localhost:5000/generate_labels_pdf/123",
|
||||
"printer_name": "default",
|
||||
"copies": 1,
|
||||
"silent": true,
|
||||
"order_id": "123",
|
||||
"quantity": "10"
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Print job sent successfully",
|
||||
"job_id": "print_20250921_103000",
|
||||
"printer": "HP LaserJet Pro",
|
||||
"timestamp": "2025-09-21T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 Technical Details
|
||||
|
||||
### Service Architecture
|
||||
- **Framework**: Flask (Python)
|
||||
- **Service Type**: Windows Service (pywin32)
|
||||
- **Communication**: HTTP REST API + Native Messaging
|
||||
- **Security**: Localhost binding only (127.0.0.1:8765)
|
||||
|
||||
### Chrome Extension Architecture
|
||||
- **Manifest Version**: 3
|
||||
- **Service Worker**: Handles background print requests
|
||||
- **Content Script**: Integrates with Quality Recticel web pages
|
||||
- **Native Messaging**: Communicates with Windows service
|
||||
|
||||
### Security Considerations
|
||||
- Service only accepts local connections (localhost)
|
||||
- No external network access required
|
||||
- Chrome extension runs in sandboxed environment
|
||||
- Windows service runs with system privileges (required for printing)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Start Checklist
|
||||
|
||||
- [ ] Download `windows_print_service` folder
|
||||
- [ ] Right-click `install_service.bat` → "Run as administrator"
|
||||
- [ ] Install Chrome extension from `chrome_extension` folder
|
||||
- [ ] Verify service at `http://localhost:8765/health`
|
||||
- [ ] Test printing from Quality Recticel web application
|
||||
|
||||
**Installation Time**: ~5 minutes
|
||||
**User Training Required**: Minimal (automatic detection and fallback)
|
||||
**Maintenance**: Zero (auto-starts with Windows)
|
||||
|
||||
For additional support, check the log files and diagnostic commands listed above.
|
||||
@@ -1,69 +0,0 @@
|
||||
# 🚀 Quality Recticel Print Service - Quick Setup
|
||||
|
||||
## 📦 What You Get
|
||||
- **Silent PDF Printing** - No more manual downloads!
|
||||
- **Automatic Detection** - Smart fallback when service unavailable
|
||||
- **Zero Configuration** - Works out of the box
|
||||
|
||||
## ⚡ 2-Minute Installation
|
||||
|
||||
### Step 1: Install Windows Service
|
||||
1. **Right-click** `install_service.bat`
|
||||
2. Select **"Run as administrator"**
|
||||
3. Click **"Yes"** and wait for completion
|
||||
|
||||
### Step 2: Install Chrome Extension
|
||||
1. Open Chrome → `chrome://extensions/`
|
||||
2. Enable **"Developer mode"**
|
||||
3. Click **"Load unpacked"** → Select `chrome_extension` folder
|
||||
|
||||
### Step 3: Verify Installation
|
||||
- Visit: `http://localhost:8765/health`
|
||||
- Should see: `{"status": "healthy"}`
|
||||
|
||||
## 🎯 How It Works
|
||||
|
||||
| Service Status | Button Appearance | What Happens |
|
||||
|---------------|-------------------|--------------|
|
||||
| **Running** ✅ | 🖨️ **Print Labels (Silent)** (Green) | Direct printing |
|
||||
| **Not Running** ❌ | 📄 **Generate PDF** (Blue) | PDF download |
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| **Service won't start** | Run `install_service.bat` as Administrator |
|
||||
| **Chrome extension not working** | Reload extension in `chrome://extensions/` |
|
||||
| **Can't connect to localhost:8765** | Check Windows Firewall (port 8765) |
|
||||
| **Nothing prints** | Verify default printer is set up |
|
||||
|
||||
## 🔧 Management Commands
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
python service_manager.py status
|
||||
|
||||
# Restart service
|
||||
python service_manager.py restart
|
||||
|
||||
# Uninstall service
|
||||
python service_manager.py uninstall
|
||||
```
|
||||
|
||||
## 📍 Important Notes
|
||||
|
||||
- ⚡ **Auto-starts** with Windows - no manual intervention needed
|
||||
- 🔒 **Local only** - service only accessible from same computer
|
||||
- 🖨️ **Uses default printer** - configure your default printer in Windows
|
||||
- 💾 **Don't move files** after installation - keep folder in same location
|
||||
|
||||
## 🆘 Quick Support
|
||||
|
||||
**Service API**: `http://localhost:8765`
|
||||
**Health Check**: `http://localhost:8765/health`
|
||||
**Printer List**: `http://localhost:8765/printers`
|
||||
|
||||
**Log File**: `print_service.log` (same folder as installation)
|
||||
|
||||
---
|
||||
*Installation takes ~5 minutes • Zero maintenance required • Works with existing Quality Recticel web application*
|
||||
@@ -1,348 +0,0 @@
|
||||
# Quality Recticel Windows Print Service
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
Local Windows service providing REST API for silent PDF printing via Chrome extension integration.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Quality Recticel Web App │
|
||||
│ (print_module.html) │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│ HTTP Request
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Windows Print Service │
|
||||
│ (localhost:8765) │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Flask │ │ CORS │ │ PDF Handler │ │
|
||||
│ │ Server │ │ Support │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│ Native Messaging
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Chrome Extension │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Background │ │ Content │ │ Popup │ │
|
||||
│ │ Service │ │ Script │ │ UI │ │
|
||||
│ │ Worker │ │ │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│ Windows API
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Windows Print System │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
windows_print_service/
|
||||
├── 📄 print_service.py # Main Flask service
|
||||
├── 📄 service_manager.py # Windows service wrapper
|
||||
├── 📄 install_service.bat # Installation script
|
||||
├── 📄 INSTALLATION_GUIDE.md # Complete documentation
|
||||
├── 📄 QUICK_SETUP.md # User quick reference
|
||||
├── 📄 README.md # This file
|
||||
└── 📁 chrome_extension/ # Chrome extension
|
||||
├── 📄 manifest.json # Extension manifest v3
|
||||
├── 📄 background.js # Service worker
|
||||
├── 📄 content.js # Page content integration
|
||||
├── 📄 popup.html # Extension popup UI
|
||||
├── 📄 popup.js # Popup functionality
|
||||
└── 📁 icons/ # Extension icons
|
||||
```
|
||||
|
||||
## 🚀 API Endpoints
|
||||
|
||||
### Base URL: `http://localhost:8765`
|
||||
|
||||
| Endpoint | Method | Description | Request Body | Response |
|
||||
|----------|--------|-------------|--------------|----------|
|
||||
| `/health` | GET | Service health check | None | `{"status": "healthy", ...}` |
|
||||
| `/printers` | GET | List available printers | None | `{"printers": [...]}` |
|
||||
| `/print/pdf` | POST | Print PDF from URL | `{"url": "...", "printer": "..."}` | `{"success": true, ...}` |
|
||||
| `/print/silent` | POST | Silent print with metadata | `{"pdf_url": "...", "order_id": "..."}` | `{"success": true, ...}` |
|
||||
|
||||
### Example API Usage
|
||||
|
||||
```javascript
|
||||
// Health Check
|
||||
const health = await fetch('http://localhost:8765/health');
|
||||
const status = await health.json();
|
||||
|
||||
// Silent Print
|
||||
const printRequest = {
|
||||
pdf_url: 'http://localhost:5000/generate_labels_pdf/123',
|
||||
printer_name: 'default',
|
||||
copies: 1,
|
||||
silent: true,
|
||||
order_id: '123',
|
||||
quantity: '10'
|
||||
};
|
||||
|
||||
const response = await fetch('http://localhost:8765/print/silent', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(printRequest)
|
||||
});
|
||||
```
|
||||
|
||||
## 🔧 Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.8+
|
||||
- Windows 10/11
|
||||
- Chrome Browser
|
||||
- Administrator privileges
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Clone/download the project
|
||||
cd windows_print_service
|
||||
|
||||
# Install dependencies
|
||||
pip install flask flask-cors requests pywin32
|
||||
|
||||
# Run development server (not as service)
|
||||
python print_service.py
|
||||
|
||||
# Install as Windows service
|
||||
python service_manager.py install
|
||||
|
||||
# Service management
|
||||
python service_manager.py start
|
||||
python service_manager.py stop
|
||||
python service_manager.py restart
|
||||
python service_manager.py uninstall
|
||||
```
|
||||
|
||||
### Chrome Extension Development
|
||||
|
||||
```bash
|
||||
# Load extension in Chrome
|
||||
chrome://extensions/ → Developer mode ON → Load unpacked
|
||||
|
||||
# Debug extension
|
||||
chrome://extensions/ → Details → Background page (for service worker)
|
||||
chrome://extensions/ → Details → Inspect views (for popup)
|
||||
```
|
||||
|
||||
## 📋 Configuration
|
||||
|
||||
### Service Configuration (`print_service.py`)
|
||||
|
||||
```python
|
||||
class WindowsPrintService:
|
||||
def __init__(self, host='127.0.0.1', port=8765):
|
||||
self.host = host # Localhost binding only
|
||||
self.port = port # Service port
|
||||
self.app = Flask(__name__)
|
||||
```
|
||||
|
||||
### Chrome Extension Permissions (`manifest.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"printing", // Access to printer API
|
||||
"nativeMessaging", // Communication with Windows service
|
||||
"activeTab", // Current tab access
|
||||
"storage" // Extension settings storage
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Integration Flow
|
||||
|
||||
### 1. Service Detection
|
||||
```javascript
|
||||
// Web page detects service availability
|
||||
const isServiceAvailable = await checkServiceHealth();
|
||||
updatePrintButton(isServiceAvailable);
|
||||
```
|
||||
|
||||
### 2. Print Request Flow
|
||||
```
|
||||
User clicks print → Web app → Windows service → Chrome extension → Printer
|
||||
```
|
||||
|
||||
### 3. Fallback Mechanism
|
||||
```
|
||||
Service unavailable → Fallback to PDF download → Manual printing
|
||||
```
|
||||
|
||||
## 🛠️ Customization
|
||||
|
||||
### Adding New Print Options
|
||||
|
||||
```python
|
||||
# In print_service.py
|
||||
@app.route('/print/custom', methods=['POST'])
|
||||
def print_custom():
|
||||
data = request.json
|
||||
# Custom print logic here
|
||||
return jsonify({'success': True})
|
||||
```
|
||||
|
||||
### Modifying Chrome Extension
|
||||
|
||||
```javascript
|
||||
// In background.js - Add new message handler
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'CUSTOM_PRINT') {
|
||||
// Custom print logic
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Web Application Integration
|
||||
|
||||
```javascript
|
||||
// In print_module.html - Modify print function
|
||||
async function customPrintFunction(orderId) {
|
||||
const response = await fetch('http://localhost:8765/print/custom', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({orderId, customOptions: {...}})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Unit Tests (Future Enhancement)
|
||||
|
||||
```python
|
||||
# test_print_service.py
|
||||
import unittest
|
||||
from print_service import WindowsPrintService
|
||||
|
||||
class TestPrintService(unittest.TestCase):
|
||||
def test_health_endpoint(self):
|
||||
# Test implementation
|
||||
pass
|
||||
```
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [ ] Service starts automatically on Windows boot
|
||||
- [ ] API endpoints respond correctly
|
||||
- [ ] Chrome extension loads without errors
|
||||
- [ ] Print jobs execute successfully
|
||||
- [ ] Fallback works when service unavailable
|
||||
- [ ] Firewall allows port 8765 traffic
|
||||
|
||||
## 📊 Monitoring & Logging
|
||||
|
||||
### Log Files
|
||||
- **Service Log**: `print_service.log` (Flask application logs)
|
||||
- **Windows Event Log**: Windows Services logs
|
||||
- **Chrome DevTools**: Extension console logs
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
```python
|
||||
# Monitor service health
|
||||
import requests
|
||||
try:
|
||||
response = requests.get('http://localhost:8765/health', timeout=5)
|
||||
if response.status_code == 200:
|
||||
print("✅ Service healthy")
|
||||
except:
|
||||
print("❌ Service unavailable")
|
||||
```
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
### Network Security
|
||||
- **Localhost Only**: Service binds to 127.0.0.1 (no external access)
|
||||
- **No Authentication**: Relies on local machine security
|
||||
- **Firewall Rule**: Port 8765 opened for local connections only
|
||||
|
||||
### Chrome Extension Security
|
||||
- **Manifest V3**: Latest security standards
|
||||
- **Minimal Permissions**: Only necessary permissions requested
|
||||
- **Sandboxed**: Runs in Chrome's security sandbox
|
||||
|
||||
### Windows Service Security
|
||||
- **System Service**: Runs with appropriate Windows service privileges
|
||||
- **Print Permissions**: Requires printer access (normal for print services)
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Production Deployment
|
||||
|
||||
1. **Package Distribution**:
|
||||
```bash
|
||||
# Create deployment package
|
||||
zip -r quality_recticel_print_service.zip windows_print_service/
|
||||
```
|
||||
|
||||
2. **Installation Script**: Use `install_service.bat` for end users
|
||||
|
||||
3. **Group Policy Deployment**: Deploy Chrome extension via enterprise policies
|
||||
|
||||
### Enterprise Considerations
|
||||
|
||||
- **Silent Installation**: Modify `install_service.bat` for unattended install
|
||||
- **Registry Deployment**: Pre-configure Chrome extension registry entries
|
||||
- **Network Policies**: Ensure firewall policies allow localhost:8765
|
||||
|
||||
## 📚 Dependencies
|
||||
|
||||
### Python Packages
|
||||
```
|
||||
flask>=2.3.0 # Web framework
|
||||
flask-cors>=4.0.0 # CORS support
|
||||
requests>=2.31.0 # HTTP client
|
||||
pywin32>=306 # Windows service integration
|
||||
```
|
||||
|
||||
### Chrome APIs
|
||||
- `chrome.printing.*` - Printing functionality
|
||||
- `chrome.runtime.*` - Extension messaging
|
||||
- `chrome.nativeMessaging.*` - Native app communication
|
||||
|
||||
## 🐛 Debugging
|
||||
|
||||
### Common Debug Commands
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
sc query QualityRecticelPrintService
|
||||
|
||||
# Test API manually
|
||||
curl http://localhost:8765/health
|
||||
|
||||
# Check listening ports
|
||||
netstat -an | findstr :8765
|
||||
|
||||
# View service logs
|
||||
type print_service.log
|
||||
```
|
||||
|
||||
### Chrome Extension Debugging
|
||||
|
||||
```javascript
|
||||
// In background.js - Add debug logging
|
||||
console.log('Print request received:', message);
|
||||
|
||||
// In popup.js - Test API connection
|
||||
fetch('http://localhost:8765/health')
|
||||
.then(r => r.json())
|
||||
.then(data => console.log('Service status:', data));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License & Support
|
||||
|
||||
**Project**: Quality Recticel Print Service
|
||||
**Version**: 1.0
|
||||
**Compatibility**: Windows 10/11, Chrome 88+
|
||||
**Maintenance**: Zero-maintenance after installation
|
||||
|
||||
For technical support, refer to `INSTALLATION_GUIDE.md` troubleshooting section.
|
||||
539
py_app/app/static/fg_quality.js
Normal file
539
py_app/app/static/fg_quality.js
Normal file
@@ -0,0 +1,539 @@
|
||||
// FG Quality specific JavaScript - Standalone version
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prevent conflicts with main script.js by removing existing listeners
|
||||
console.log('FG Quality JavaScript loaded');
|
||||
|
||||
const reportButtons = document.querySelectorAll('.report-btn');
|
||||
const reportTable = document.getElementById('report-table');
|
||||
const reportTitle = document.getElementById('report-title');
|
||||
const exportCsvButton = document.getElementById('export-csv');
|
||||
|
||||
// Calendar elements
|
||||
const calendarModal = document.getElementById('calendar-modal');
|
||||
const dateRangeModal = document.getElementById('date-range-modal');
|
||||
const selectDayReport = document.getElementById('select-day-report');
|
||||
const selectDayDefectsReport = document.getElementById('select-day-defects-report');
|
||||
const dateRangeReport = document.getElementById('date-range-report');
|
||||
const dateRangeDefectsReport = document.getElementById('date-range-defects-report');
|
||||
|
||||
let currentReportType = null;
|
||||
let currentDate = new Date();
|
||||
let selectedDate = null;
|
||||
|
||||
// Clear any existing event listeners by cloning elements
|
||||
function clearExistingListeners() {
|
||||
if (selectDayReport) {
|
||||
const newSelectDayReport = selectDayReport.cloneNode(true);
|
||||
selectDayReport.parentNode.replaceChild(newSelectDayReport, selectDayReport);
|
||||
}
|
||||
if (selectDayDefectsReport) {
|
||||
const newSelectDayDefectsReport = selectDayDefectsReport.cloneNode(true);
|
||||
selectDayDefectsReport.parentNode.replaceChild(newSelectDayDefectsReport, selectDayDefectsReport);
|
||||
}
|
||||
if (dateRangeReport) {
|
||||
const newDateRangeReport = dateRangeReport.cloneNode(true);
|
||||
dateRangeReport.parentNode.replaceChild(newDateRangeReport, dateRangeReport);
|
||||
}
|
||||
if (dateRangeDefectsReport) {
|
||||
const newDateRangeDefectsReport = dateRangeDefectsReport.cloneNode(true);
|
||||
dateRangeDefectsReport.parentNode.replaceChild(newDateRangeDefectsReport, dateRangeDefectsReport);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing listeners first
|
||||
clearExistingListeners();
|
||||
|
||||
// Re-get elements after cloning
|
||||
const newSelectDayReport = document.getElementById('select-day-report');
|
||||
const newSelectDayDefectsReport = document.getElementById('select-day-defects-report');
|
||||
const newDateRangeReport = document.getElementById('date-range-report');
|
||||
const newDateRangeDefectsReport = document.getElementById('date-range-defects-report');
|
||||
|
||||
// Add event listeners to report buttons
|
||||
reportButtons.forEach(button => {
|
||||
const reportType = button.getAttribute('data-report');
|
||||
if (reportType) {
|
||||
// Clone to remove existing listeners
|
||||
const newButton = button.cloneNode(true);
|
||||
button.parentNode.replaceChild(newButton, button);
|
||||
|
||||
newButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('FG Report button clicked:', reportType);
|
||||
fetchFGReportData(reportType);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Calendar-based report buttons with FG-specific handlers
|
||||
if (newSelectDayReport) {
|
||||
newSelectDayReport.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('FG Select Day Report clicked');
|
||||
currentReportType = '6';
|
||||
showCalendarModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (newSelectDayDefectsReport) {
|
||||
newSelectDayDefectsReport.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('FG Select Day Defects Report clicked');
|
||||
currentReportType = '8';
|
||||
showCalendarModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (newDateRangeReport) {
|
||||
newDateRangeReport.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('FG Date Range Report clicked');
|
||||
currentReportType = '7';
|
||||
showDateRangeModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (newDateRangeDefectsReport) {
|
||||
newDateRangeDefectsReport.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('FG Date Range Defects Report clicked');
|
||||
currentReportType = '9';
|
||||
showDateRangeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Function to fetch FG report data
|
||||
function fetchFGReportData(reportType) {
|
||||
const url = `/get_fg_report_data?report=${reportType}`;
|
||||
console.log('Fetching FG data from:', url);
|
||||
reportTitle.textContent = 'Loading FG data...';
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('FG Report data received:', data);
|
||||
if (data.error) {
|
||||
reportTitle.textContent = data.error;
|
||||
return;
|
||||
}
|
||||
|
||||
populateFGTable(data);
|
||||
updateReportTitle(reportType);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching FG report data:', error);
|
||||
reportTitle.textContent = 'Error loading FG data.';
|
||||
});
|
||||
}
|
||||
|
||||
// Function to fetch FG report data for specific dates
|
||||
function fetchFGDateReportData(reportType, date, startDate = null, endDate = null) {
|
||||
let url = `/generate_fg_report?report=${reportType}`;
|
||||
if (date) {
|
||||
url += `&date=${date}`;
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
url += `&start_date=${startDate}&end_date=${endDate}`;
|
||||
}
|
||||
|
||||
console.log('Fetching FG date report from:', url);
|
||||
reportTitle.textContent = 'Loading FG data...';
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('FG Date report data received:', data);
|
||||
if (data.error) {
|
||||
reportTitle.textContent = data.error;
|
||||
return;
|
||||
}
|
||||
|
||||
populateFGTable(data);
|
||||
updateDateReportTitle(reportType, date, startDate, endDate);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching FG date report data:', error);
|
||||
reportTitle.textContent = 'Error loading FG data.';
|
||||
});
|
||||
}
|
||||
|
||||
// Function to populate the table with FG data
|
||||
function populateFGTable(data) {
|
||||
const thead = reportTable.querySelector('thead tr');
|
||||
const tbody = reportTable.querySelector('tbody');
|
||||
|
||||
// Clear existing content
|
||||
thead.innerHTML = '';
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Add headers
|
||||
if (data.headers && data.headers.length > 0) {
|
||||
data.headers.forEach(header => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = header;
|
||||
thead.appendChild(th);
|
||||
});
|
||||
}
|
||||
|
||||
// Add rows
|
||||
if (data.rows && data.rows.length > 0) {
|
||||
data.rows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
row.forEach(cell => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = cell || '';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
// Show no data message
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.colSpan = data.headers ? data.headers.length : 1;
|
||||
td.textContent = data.message || 'No FG data found for the selected criteria.';
|
||||
td.style.textAlign = 'center';
|
||||
td.style.fontStyle = 'italic';
|
||||
td.style.padding = '20px';
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update report title based on type
|
||||
function updateReportTitle(reportType) {
|
||||
const titles = {
|
||||
'1': 'Daily Complete FG Orders Report',
|
||||
'2': '5-Day Complete FG Orders Report',
|
||||
'3': 'FG Items with Defects for Current Day',
|
||||
'4': 'FG Items with Defects for Last 5 Days',
|
||||
'5': 'Complete FG Database Report'
|
||||
};
|
||||
|
||||
reportTitle.textContent = titles[reportType] || 'FG Quality Report';
|
||||
}
|
||||
|
||||
// Function to update report title for date-based reports
|
||||
function updateDateReportTitle(reportType, date, startDate, endDate) {
|
||||
const titles = {
|
||||
'6': `FG Daily Report for ${date}`,
|
||||
'7': `FG Date Range Report (${startDate} to ${endDate})`,
|
||||
'8': `FG Quality Defects Report for ${date}`,
|
||||
'9': `FG Quality Defects Range Report (${startDate} to ${endDate})`
|
||||
};
|
||||
|
||||
reportTitle.textContent = titles[reportType] || 'FG Quality Report';
|
||||
}
|
||||
|
||||
// Calendar functionality
|
||||
function showCalendarModal() {
|
||||
if (calendarModal) {
|
||||
calendarModal.style.display = 'block';
|
||||
generateCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
function hideCalendarModal() {
|
||||
if (calendarModal) {
|
||||
calendarModal.style.display = 'none';
|
||||
selectedDate = null;
|
||||
updateConfirmButton();
|
||||
}
|
||||
}
|
||||
|
||||
function showDateRangeModal() {
|
||||
if (dateRangeModal) {
|
||||
dateRangeModal.style.display = 'block';
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('start-date').value = today;
|
||||
document.getElementById('end-date').value = today;
|
||||
}
|
||||
}
|
||||
|
||||
function hideDataRangeModal() {
|
||||
if (dateRangeModal) {
|
||||
dateRangeModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function generateCalendar() {
|
||||
const calendarDays = document.getElementById('calendar-days');
|
||||
const monthYear = document.getElementById('calendar-month-year');
|
||||
|
||||
if (!calendarDays || !monthYear) return;
|
||||
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
|
||||
monthYear.textContent = `${currentDate.toLocaleString('default', { month: 'long' })} ${year}`;
|
||||
|
||||
// Clear previous days
|
||||
calendarDays.innerHTML = '';
|
||||
|
||||
// Get first day of month and number of days
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
// Add empty cells for previous month
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
const emptyDay = document.createElement('div');
|
||||
emptyDay.className = 'calendar-day empty';
|
||||
calendarDays.appendChild(emptyDay);
|
||||
}
|
||||
|
||||
// Add days of current month
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dayElement = document.createElement('div');
|
||||
dayElement.className = 'calendar-day';
|
||||
dayElement.textContent = day;
|
||||
|
||||
// Check if it's today
|
||||
const today = new Date();
|
||||
if (year === today.getFullYear() && month === today.getMonth() && day === today.getDate()) {
|
||||
dayElement.classList.add('today');
|
||||
}
|
||||
|
||||
dayElement.addEventListener('click', () => {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.calendar-day.selected').forEach(el => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selection to clicked day
|
||||
dayElement.classList.add('selected');
|
||||
|
||||
// Set selected date
|
||||
selectedDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
console.log('FG Calendar date selected:', selectedDate);
|
||||
updateConfirmButton();
|
||||
});
|
||||
|
||||
calendarDays.appendChild(dayElement);
|
||||
}
|
||||
}
|
||||
|
||||
function updateConfirmButton() {
|
||||
const confirmButton = document.getElementById('confirm-date');
|
||||
if (confirmButton) {
|
||||
confirmButton.disabled = !selectedDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar navigation
|
||||
const prevMonthBtn = document.getElementById('prev-month');
|
||||
const nextMonthBtn = document.getElementById('next-month');
|
||||
|
||||
if (prevMonthBtn) {
|
||||
// Clone to remove existing listeners
|
||||
const newPrevBtn = prevMonthBtn.cloneNode(true);
|
||||
prevMonthBtn.parentNode.replaceChild(newPrevBtn, prevMonthBtn);
|
||||
|
||||
newPrevBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||
generateCalendar();
|
||||
});
|
||||
}
|
||||
|
||||
if (nextMonthBtn) {
|
||||
// Clone to remove existing listeners
|
||||
const newNextBtn = nextMonthBtn.cloneNode(true);
|
||||
nextMonthBtn.parentNode.replaceChild(newNextBtn, nextMonthBtn);
|
||||
|
||||
newNextBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
generateCalendar();
|
||||
});
|
||||
}
|
||||
|
||||
// Calendar modal buttons
|
||||
const cancelDateBtn = document.getElementById('cancel-date');
|
||||
const confirmDateBtn = document.getElementById('confirm-date');
|
||||
|
||||
if (cancelDateBtn) {
|
||||
// Clone to remove existing listeners
|
||||
const newCancelBtn = cancelDateBtn.cloneNode(true);
|
||||
cancelDateBtn.parentNode.replaceChild(newCancelBtn, cancelDateBtn);
|
||||
|
||||
newCancelBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hideCalendarModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (confirmDateBtn) {
|
||||
// Clone to remove existing listeners
|
||||
const newConfirmBtn = confirmDateBtn.cloneNode(true);
|
||||
confirmDateBtn.parentNode.replaceChild(newConfirmBtn, confirmDateBtn);
|
||||
|
||||
newConfirmBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('FG Calendar confirm clicked with date:', selectedDate, 'report type:', currentReportType);
|
||||
if (selectedDate && currentReportType) {
|
||||
fetchFGDateReportData(currentReportType, selectedDate);
|
||||
hideCalendarModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Date range modal buttons
|
||||
const cancelDateRangeBtn = document.getElementById('cancel-date-range');
|
||||
const confirmDateRangeBtn = document.getElementById('confirm-date-range');
|
||||
|
||||
if (cancelDateRangeBtn) {
|
||||
// Clone to remove existing listeners
|
||||
const newCancelRangeBtn = cancelDateRangeBtn.cloneNode(true);
|
||||
cancelDateRangeBtn.parentNode.replaceChild(newCancelRangeBtn, cancelDateRangeBtn);
|
||||
|
||||
newCancelRangeBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hideDataRangeModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (confirmDateRangeBtn) {
|
||||
// Clone to remove existing listeners
|
||||
const newConfirmRangeBtn = confirmDateRangeBtn.cloneNode(true);
|
||||
confirmDateRangeBtn.parentNode.replaceChild(newConfirmRangeBtn, confirmDateRangeBtn);
|
||||
|
||||
newConfirmRangeBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const startDate = document.getElementById('start-date').value;
|
||||
const endDate = document.getElementById('end-date').value;
|
||||
|
||||
console.log('FG Date range confirm clicked:', startDate, 'to', endDate, 'report type:', currentReportType);
|
||||
if (startDate && endDate && currentReportType) {
|
||||
fetchFGDateReportData(currentReportType, null, startDate, endDate);
|
||||
hideDataRangeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enable/disable date range confirm button
|
||||
const startDateInput = document.getElementById('start-date');
|
||||
const endDateInput = document.getElementById('end-date');
|
||||
|
||||
function updateDateRangeConfirmButton() {
|
||||
const confirmBtn = document.getElementById('confirm-date-range');
|
||||
if (confirmBtn && startDateInput && endDateInput) {
|
||||
confirmBtn.disabled = !startDateInput.value || !endDateInput.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (startDateInput) {
|
||||
startDateInput.addEventListener('change', updateDateRangeConfirmButton);
|
||||
}
|
||||
|
||||
if (endDateInput) {
|
||||
endDateInput.addEventListener('change', updateDateRangeConfirmButton);
|
||||
}
|
||||
|
||||
// Close modals when clicking outside
|
||||
window.addEventListener('click', (event) => {
|
||||
if (event.target === calendarModal) {
|
||||
hideCalendarModal();
|
||||
}
|
||||
if (event.target === dateRangeModal) {
|
||||
hideDataRangeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modals with X button
|
||||
document.querySelectorAll('.close-modal').forEach(closeBtn => {
|
||||
// Clone to remove existing listeners
|
||||
const newCloseBtn = closeBtn.cloneNode(true);
|
||||
closeBtn.parentNode.replaceChild(newCloseBtn, closeBtn);
|
||||
|
||||
newCloseBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const modal = event.target.closest('.modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export functionality
|
||||
if (exportCsvButton) {
|
||||
exportCsvButton.addEventListener('click', () => {
|
||||
const rows = reportTable.querySelectorAll('tr');
|
||||
if (rows.length === 0) {
|
||||
alert('No FG data available to export.');
|
||||
return;
|
||||
}
|
||||
const reportTitleText = reportTitle.textContent.trim();
|
||||
const filename = `${reportTitleText.replace(/\s+/g, '_')}.csv`;
|
||||
exportTableToCSV(filename);
|
||||
});
|
||||
}
|
||||
|
||||
// Export to CSV function
|
||||
function exportTableToCSV(filename) {
|
||||
const table = reportTable;
|
||||
const rows = Array.from(table.querySelectorAll('tr'));
|
||||
|
||||
const csvContent = rows.map(row => {
|
||||
const cells = Array.from(row.querySelectorAll('th, td'));
|
||||
return cells.map(cell => {
|
||||
let text = cell.textContent.trim();
|
||||
// Escape quotes and wrap in quotes if necessary
|
||||
if (text.includes(',') || text.includes('"') || text.includes('\n')) {
|
||||
text = '"' + text.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return text;
|
||||
}).join(',');
|
||||
}).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
// Test Database Button
|
||||
const testDatabaseBtn = document.getElementById('test-database');
|
||||
if (testDatabaseBtn) {
|
||||
testDatabaseBtn.addEventListener('click', () => {
|
||||
console.log('Testing FG database connection...');
|
||||
reportTitle.textContent = 'Testing FG Database Connection...';
|
||||
fetch('/test_fg_database')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('FG Database test results:', data);
|
||||
if (data.success) {
|
||||
reportTitle.textContent = `FG Database Test Results - ${data.total_records} records found`;
|
||||
// Show alert with summary
|
||||
alert(`FG Database Test Complete!\n\nConnection: ${data.database_connection}\nTable exists: ${data.table_exists}\nTotal records: ${data.total_records}\nMessage: ${data.message}`);
|
||||
} else {
|
||||
reportTitle.textContent = 'FG Database Test Failed';
|
||||
alert(`FG Database test failed: ${data.message}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('FG Database test error:', error);
|
||||
reportTitle.textContent = 'Error testing FG database.';
|
||||
alert('Error testing FG database connection.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('FG Quality JavaScript setup complete');
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
QZ TRAY LIBRARY PATCH NOTES
|
||||
===========================
|
||||
Version: 2.2.4 (patched for custom QZ Tray with pairing key authentication)
|
||||
Date: October 2, 2025
|
||||
|
||||
CHANGES MADE:
|
||||
-------------
|
||||
|
||||
1. Line ~387: Commented out certificate sending
|
||||
- Original: _qz.websocket.connection.sendData({ certificate: cert, promise: openPromise });
|
||||
- Patched: openPromise.resolve(); (resolves immediately without sending certificate)
|
||||
|
||||
2. Line ~391-403: Bypassed certificate retrieval
|
||||
- Original: Called _qz.security.callCert() to get certificate from user
|
||||
- Patched: Directly calls sendCert(null) without trying to get certificate
|
||||
|
||||
3. Comments added to indicate patches
|
||||
|
||||
REASON FOR PATCHES:
|
||||
------------------
|
||||
The custom QZ Tray server has certificate validation COMPLETELY DISABLED.
|
||||
It uses ONLY pairing key (HMAC) authentication instead of certificates.
|
||||
The original qz-tray.js library expects certificate-based authentication and
|
||||
fails when the server doesn't respond to certificate requests.
|
||||
|
||||
COMPATIBILITY:
|
||||
-------------
|
||||
- Works with custom QZ Tray server (forked version with certificate validation disabled)
|
||||
- NOT compatible with standard QZ Tray servers
|
||||
- Connects to both ws://localhost:8181 and wss://localhost:8182
|
||||
- Authentication handled by server-side pairing keys
|
||||
|
||||
BACKUP:
|
||||
-------
|
||||
Original unpatched version saved as: qz-tray.js.backup
|
||||
@@ -4,11 +4,16 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Flask App{% endblock %}</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<!-- Base CSS for common styles -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
|
||||
<!-- Legacy CSS for backward compatibility (temporarily) -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<!-- Page-specific CSS -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="light-mode">
|
||||
@@ -56,5 +61,8 @@
|
||||
{% if request.endpoint != 'main.fg_quality' %}
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bootstrap JavaScript -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
440
py_app/app/templates/fg_quality.html
Normal file
440
py_app/app/templates/fg_quality.html
Normal file
@@ -0,0 +1,440 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}FG Quality Module{% endblock %}
|
||||
{% block content %}
|
||||
<div class="scan-container">
|
||||
<!-- Reports Card -->
|
||||
<div class="card report-form-card">
|
||||
<h3>FG Quality Reports</h3>
|
||||
|
||||
<!-- Reports List with Label-Button Layout -->
|
||||
<div class="reports-grid">
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Daily report will export all FG orders scanned at quality scanning points</label>
|
||||
<button class="btn report-btn" data-report="1">Daily Complete FG Orders Report</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Select day for daily FG report will export all orders scanned at quality scanning points</label>
|
||||
<button class="btn report-btn" id="select-day-report">Select Day Daily FG Report</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Select date range for custom FG report - from start date 00:00 to end date 23:59</label>
|
||||
<button class="btn report-btn" id="date-range-report">Date Range FG Report</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">5-day report will export all FG orders scanned at quality scanning points</label>
|
||||
<button class="btn report-btn" data-report="2">5-Day Complete FG Orders Report</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Report on FG items with quality issues for the current day</label>
|
||||
<button class="btn report-btn" data-report="3">Report on FG Items with Defects for the Current Day</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Select specific day for FG quality defects report - items with quality issues</label>
|
||||
<button class="btn report-btn" id="select-day-defects-report">Select Day FG Quality Defects Report</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Select date range for FG quality defects report - items with quality issues between two dates</label>
|
||||
<button class="btn report-btn" id="date-range-defects-report">Date Range FG Quality Defects Report</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Report on FG items with quality issues for the last 5 days</label>
|
||||
<button class="btn report-btn" data-report="4">Report on FG Items with Defects for the Last 5 Days</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Report all FG entries from the database</label>
|
||||
<button class="btn report-btn" data-report="5">Report FG Database</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Separator -->
|
||||
<div class="report-separator"></div>
|
||||
|
||||
<!-- Export Section -->
|
||||
<div class="export-section">
|
||||
<div class="form-centered last-buttons">
|
||||
<label class="export-description">Export current report as:</label>
|
||||
<div class="button-row">
|
||||
<button class="btn export-btn" id="export-csv">Export CSV</button>
|
||||
<!-- <button class="btn export-btn" id="export-excel">Export excell</button> -->
|
||||
{% if session.get('role') == 'superadmin' %}
|
||||
<button class="btn export-btn test-db-btn" id="test-database">Test FG Database</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Display Card -->
|
||||
<div class="card report-table-card">
|
||||
<h3 id="report-title">No data to display, please select a report.</h3>
|
||||
<div class="report-table-container">
|
||||
<table class="scan-table" id="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- Table headers will be dynamically populated -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Table data will be dynamically populated -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Popup Modal -->
|
||||
<div id="calendar-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Select Date for Daily FG Report</h4>
|
||||
<span class="close-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<button id="prev-month" class="calendar-nav"><</button>
|
||||
<h3 id="calendar-month-year"></h3>
|
||||
<button id="next-month" class="calendar-nav">></button>
|
||||
</div>
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-weekdays">
|
||||
<div>Sun</div>
|
||||
<div>Mon</div>
|
||||
<div>Tue</div>
|
||||
<div>Wed</div>
|
||||
<div>Thu</div>
|
||||
<div>Fri</div>
|
||||
<div>Sat</div>
|
||||
</div>
|
||||
<div class="calendar-days" id="calendar-days">
|
||||
<!-- Days will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="cancel-date">Cancel</button>
|
||||
<button class="btn btn-primary" id="confirm-date" disabled>Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Popup Modal -->
|
||||
<div id="date-range-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Select Date Range for FG Report</h4>
|
||||
<span class="close-modal" id="close-date-range">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="date-range-container">
|
||||
<div class="date-input-group">
|
||||
<label for="start-date">Start Date:</label>
|
||||
<input type="date" id="start-date" class="date-input" />
|
||||
<small class="date-help">Report will include data from 00:00:00</small>
|
||||
</div>
|
||||
<div class="date-input-group">
|
||||
<label for="end-date">End Date:</label>
|
||||
<input type="date" id="end-date" class="date-input" />
|
||||
<small class="date-help">Report will include data until 23:59:59</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="cancel-date-range">Cancel</button>
|
||||
<button class="btn btn-primary" id="confirm-date-range" disabled>Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
// Debug check - this will run immediately when the page loads
|
||||
console.log('FG Quality page loaded - checking script conflicts');
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('FG Quality DOM loaded');
|
||||
// Add a visible indicator that our script loaded
|
||||
setTimeout(() => {
|
||||
const titleElement = document.getElementById('report-title');
|
||||
if (titleElement && titleElement.textContent === 'No data to display, please select a report.') {
|
||||
titleElement.textContent = '🟢 FG Quality Module Ready - Select a report';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='fg_quality.js') }}"></script>
|
||||
<script>
|
||||
// Theme functionality for FG Quality page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const body = document.body;
|
||||
const themeToggleButton = document.getElementById('theme-toggle');
|
||||
|
||||
// Helper function to update the theme toggle button text
|
||||
function updateThemeToggleButtonText() {
|
||||
if (body.classList.contains('dark-mode')) {
|
||||
themeToggleButton.textContent = 'Change to Light Mode';
|
||||
} else {
|
||||
themeToggleButton.textContent = 'Change to Dark Mode';
|
||||
}
|
||||
}
|
||||
|
||||
// Check and apply the saved theme from localStorage
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
body.classList.toggle('dark-mode', savedTheme === 'dark');
|
||||
}
|
||||
|
||||
// Update the button text based on the current theme
|
||||
if (themeToggleButton) {
|
||||
updateThemeToggleButtonText();
|
||||
|
||||
// Toggle the theme on button click
|
||||
themeToggleButton.addEventListener('click', () => {
|
||||
const isDarkMode = body.classList.toggle('dark-mode');
|
||||
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
||||
updateThemeToggleButtonText(); // Update the button text after toggling
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Override script - runs after both script.js and fg_quality.js
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// Wait a bit to ensure all scripts have loaded
|
||||
setTimeout(() => {
|
||||
console.log('🔧 FG Quality Override Script - Fixing event handlers');
|
||||
|
||||
// Override the problematic calendar functionality
|
||||
const selectDayReport = document.getElementById('select-day-report');
|
||||
const selectDayDefectsReport = document.getElementById('select-day-defects-report');
|
||||
const dateRangeReport = document.getElementById('date-range-report');
|
||||
const dateRangeDefectsReport = document.getElementById('date-range-defects-report');
|
||||
|
||||
let currentFGReportType = null;
|
||||
let selectedFGDate = null;
|
||||
|
||||
// Clear and re-add event listeners with FG-specific functionality
|
||||
if (selectDayReport) {
|
||||
// Remove all existing listeners by cloning
|
||||
const newSelectDayReport = selectDayReport.cloneNode(true);
|
||||
selectDayReport.parentNode.replaceChild(newSelectDayReport, selectDayReport);
|
||||
|
||||
newSelectDayReport.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('🎯 FG Select Day Report clicked');
|
||||
currentFGReportType = '6';
|
||||
showFGCalendarModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (dateRangeReport) {
|
||||
const newDateRangeReport = dateRangeReport.cloneNode(true);
|
||||
dateRangeReport.parentNode.replaceChild(newDateRangeReport, dateRangeReport);
|
||||
|
||||
newDateRangeReport.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('🎯 FG Date Range Report clicked');
|
||||
currentFGReportType = '7';
|
||||
showFGDateRangeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// FG-specific calendar modal functionality
|
||||
function showFGCalendarModal() {
|
||||
const calendarModal = document.getElementById('calendar-modal');
|
||||
if (calendarModal) {
|
||||
calendarModal.style.display = 'block';
|
||||
generateFGCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
function showFGDateRangeModal() {
|
||||
const dateRangeModal = document.getElementById('date-range-modal');
|
||||
if (dateRangeModal) {
|
||||
dateRangeModal.style.display = 'block';
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('start-date').value = today;
|
||||
document.getElementById('end-date').value = today;
|
||||
}
|
||||
}
|
||||
|
||||
function generateFGCalendar() {
|
||||
const calendarDays = document.getElementById('calendar-days');
|
||||
const monthYear = document.getElementById('calendar-month-year');
|
||||
|
||||
if (!calendarDays || !monthYear) return;
|
||||
|
||||
const currentDate = new Date();
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
|
||||
monthYear.textContent = `${currentDate.toLocaleString('default', { month: 'long' })} ${year}`;
|
||||
|
||||
// Clear previous days
|
||||
calendarDays.innerHTML = '';
|
||||
|
||||
// Get first day of month and number of days
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
// Add empty cells for previous month
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
const emptyDay = document.createElement('div');
|
||||
emptyDay.className = 'calendar-day empty';
|
||||
calendarDays.appendChild(emptyDay);
|
||||
}
|
||||
|
||||
// Add days of current month
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dayElement = document.createElement('div');
|
||||
dayElement.className = 'calendar-day';
|
||||
dayElement.textContent = day;
|
||||
|
||||
// Highlight October 15 as it has FG data
|
||||
if (month === 9 && day === 15) { // October is month 9
|
||||
dayElement.style.backgroundColor = '#e3f2fd';
|
||||
dayElement.style.border = '2px solid #2196f3';
|
||||
dayElement.title = 'FG data available';
|
||||
}
|
||||
|
||||
dayElement.addEventListener('click', () => {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.calendar-day.selected').forEach(el => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selection to clicked day
|
||||
dayElement.classList.add('selected');
|
||||
|
||||
// Set selected date
|
||||
selectedFGDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
console.log('🗓️ FG Calendar date selected:', selectedFGDate);
|
||||
|
||||
// Enable confirm button
|
||||
const confirmButton = document.getElementById('confirm-date');
|
||||
if (confirmButton) {
|
||||
confirmButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
calendarDays.appendChild(dayElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Override confirm button
|
||||
const confirmDateBtn = document.getElementById('confirm-date');
|
||||
if (confirmDateBtn) {
|
||||
const newConfirmBtn = confirmDateBtn.cloneNode(true);
|
||||
confirmDateBtn.parentNode.replaceChild(newConfirmBtn, confirmDateBtn);
|
||||
|
||||
newConfirmBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('✅ FG Calendar confirm clicked:', selectedFGDate, 'Report type:', currentFGReportType);
|
||||
|
||||
if (selectedFGDate && currentFGReportType) {
|
||||
const url = `/generate_fg_report?report=${currentFGReportType}&date=${selectedFGDate}`;
|
||||
console.log('🚀 Calling FG endpoint:', url);
|
||||
|
||||
document.getElementById('report-title').textContent = 'Loading FG data...';
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('📊 FG Data received:', data);
|
||||
|
||||
const table = document.getElementById('report-table');
|
||||
const thead = table.querySelector('thead tr');
|
||||
const tbody = table.querySelector('tbody');
|
||||
|
||||
// Clear existing content
|
||||
thead.innerHTML = '';
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Add headers
|
||||
if (data.headers) {
|
||||
data.headers.forEach(header => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = header;
|
||||
thead.appendChild(th);
|
||||
});
|
||||
}
|
||||
|
||||
// Add rows
|
||||
if (data.rows && data.rows.length > 0) {
|
||||
data.rows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
row.forEach(cell => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = cell || '';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
document.getElementById('report-title').textContent = `FG Daily Report for ${selectedFGDate} (${data.rows.length} records)`;
|
||||
} else {
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.colSpan = data.headers ? data.headers.length : 1;
|
||||
td.textContent = data.message || 'No FG data found';
|
||||
td.style.textAlign = 'center';
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
document.getElementById('report-title').textContent = `FG Daily Report for ${selectedFGDate} - No data`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Error fetching FG data:', error);
|
||||
document.getElementById('report-title').textContent = 'Error loading FG data';
|
||||
});
|
||||
|
||||
// Hide modal
|
||||
document.getElementById('calendar-modal').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Override date range confirm button
|
||||
const confirmDateRangeBtn = document.getElementById('confirm-date-range');
|
||||
if (confirmDateRangeBtn) {
|
||||
const newConfirmRangeBtn = confirmDateRangeBtn.cloneNode(true);
|
||||
confirmDateRangeBtn.parentNode.replaceChild(newConfirmRangeBtn, confirmDateRangeBtn);
|
||||
|
||||
newConfirmRangeBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const startDate = document.getElementById('start-date').value;
|
||||
const endDate = document.getElementById('end-date').value;
|
||||
|
||||
console.log('📅 FG Date range confirm:', startDate, 'to', endDate);
|
||||
|
||||
if (startDate && endDate && currentFGReportType) {
|
||||
const url = `/generate_fg_report?report=${currentFGReportType}&start_date=${startDate}&end_date=${endDate}`;
|
||||
console.log('🚀 Calling FG range endpoint:', url);
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('📊 FG Range data received:', data);
|
||||
// Handle response similar to above
|
||||
document.getElementById('report-title').textContent = `FG Date Range Report (${startDate} to ${endDate})`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Error fetching FG range data:', error);
|
||||
});
|
||||
|
||||
// Hide modal
|
||||
document.getElementById('date-range-modal').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ FG Quality Override Script - Event handlers fixed');
|
||||
}, 500); // Wait 500ms to ensure all other scripts are loaded
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
40
py_app/app/templates/main_page_reports.html
Normal file
40
py_app/app/templates/main_page_reports.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Reports Module{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="reports-container">
|
||||
<h1>Reports Module</h1>
|
||||
<p>Access different quality and production reporting modules for data analysis and verification.</p>
|
||||
|
||||
<!-- Row of evenly distributed cards -->
|
||||
<div class="dashboard-container">
|
||||
<!-- Card 1: Quality Reports -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Quality Reports</h3>
|
||||
<p>Access quality scanning reports and analysis for production process verification.</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<a href="{{ url_for('main.quality') }}" class="btn">Launch Quality Reports</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: FG Quality Reports -->
|
||||
<div class="dashboard-card">
|
||||
<h3>FG Quality Reports</h3>
|
||||
<p>Finished Goods quality reports and analysis for final product verification.</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<a href="{{ url_for('main.fg_quality') }}" class="btn">Launch FG Quality Reports</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Additional Reports (Placeholder for future expansion) -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Additional Reports</h3>
|
||||
<p>Access additional reporting modules and data analysis tools.</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button class="btn" disabled style="opacity: 0.5;">Coming Soon</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,454 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Role Permissions Management{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.permissions-container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.permissions-table-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
overflow: hidden;
|
||||
margin: 0 auto 30px auto;
|
||||
border: 2px solid #dee2e6;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.permissions-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.permissions-table thead {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.permissions-table th {
|
||||
padding: 15px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.permissions-table th:nth-child(1) { width: 15%; }
|
||||
.permissions-table th:nth-child(2) { width: 20%; }
|
||||
.permissions-table th:nth-child(3) { width: 25%; }
|
||||
.permissions-table th:nth-child(4) { width: 40%; }
|
||||
|
||||
.permission-row {
|
||||
border-bottom: 2px solid #dee2e6 !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.permission-row:hover {
|
||||
background: linear-gradient(135deg, #e3f2fd, #f0f8ff) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 4px 12px rgba(0,123,255,0.15) !important;
|
||||
}
|
||||
|
||||
.role-cell, .module-cell, .page-cell, .functions-cell {
|
||||
padding: 15px 12px !important;
|
||||
vertical-align: top !important;
|
||||
border-right: 1px solid #f1f3f4 !important;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
border-left: 4px solid #007bff !important;
|
||||
}
|
||||
|
||||
.module-cell {
|
||||
border-left: 2px solid #28a745 !important;
|
||||
}
|
||||
|
||||
.page-cell {
|
||||
border-left: 2px solid #ffc107 !important;
|
||||
}
|
||||
|
||||
.functions-cell {
|
||||
border-left: 2px solid #dc3545 !important;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #e3f2fd;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.functions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.function-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + .toggle-slider {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + .toggle-slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.function-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.role-separator, .module-separator {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.separator-line {
|
||||
padding: 12px 20px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
background: linear-gradient(135deg, #e9ecef, #f8f9fa);
|
||||
}
|
||||
|
||||
.module-badge {
|
||||
padding: 8px 15px;
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
color: white;
|
||||
border-radius: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-buttons-container {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(108,117,125,0.3);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="permissions-container">
|
||||
<div style="text-align: center; margin-bottom: 40px;">
|
||||
<h1 style="color: #2c3e50; margin-bottom: 15px; font-weight: 700; font-size: 32px;">
|
||||
🔐 Role Permissions Management
|
||||
</h1>
|
||||
<p style="color: #6c757d; font-size: 16px;">
|
||||
Configure granular access permissions for each role in the system
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 4-Column Permissions Table -->
|
||||
<div class="permissions-table-container">
|
||||
<table class="permissions-table" id="permissionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>👤 Role Name</th>
|
||||
<th>🏢 Module Name</th>
|
||||
<th>📄 Page Name</th>
|
||||
<th>⚙️ Functions & Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set current_role = '' %}
|
||||
{% set current_module = '' %}
|
||||
{% for role_name, role_data in roles.items() %}
|
||||
{% for page_key, page_data in pages.items() %}
|
||||
{% for section_key, section_data in page_data.sections.items() %}
|
||||
|
||||
<!-- Role separator row -->
|
||||
{% if current_role != role_name %}
|
||||
{% set current_role = role_name %}
|
||||
<tr class="role-separator">
|
||||
<td colspan="4">
|
||||
<div class="separator-line">
|
||||
<span>{{ role_data.display_name }} (Level {{ role_data.level }})</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<!-- Module separator -->
|
||||
{% if current_module != page_key %}
|
||||
{% set current_module = page_key %}
|
||||
<tr class="module-separator">
|
||||
<td></td>
|
||||
<td colspan="3">
|
||||
<div style="padding: 8px 15px;">
|
||||
<span class="module-badge">{{ page_data.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr class="permission-row" data-role="{{ role_name }}" data-module="{{ page_key }}">
|
||||
<td class="role-cell">
|
||||
<div class="role-badge">
|
||||
<span>👤</span>
|
||||
<span>{{ role_data.display_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="module-cell">
|
||||
<span>{{ page_data.name }}</span>
|
||||
</td>
|
||||
<td class="page-cell">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>📋</span>
|
||||
<span>{{ section_data.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="functions-cell">
|
||||
<div class="functions-grid">
|
||||
{% for action in section_data.actions %}
|
||||
{% set permission_key = page_key + '.' + section_key + '.' + action %}
|
||||
<div class="function-item" data-permission="{{ permission_key }}" data-role="{{ role_name }}">
|
||||
<label class="function-toggle">
|
||||
<input type="checkbox"
|
||||
data-role="{{ role_name }}"
|
||||
data-page="{{ page_key }}"
|
||||
data-section="{{ section_key }}"
|
||||
data-action="{{ action }}"
|
||||
onchange="togglePermission('{{ role_name }}', '{{ page_key }}', '{{ section_key }}', '{{ action }}', this)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span class="function-text">{{ action_names[action] }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% set current_module = '' %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons-container">
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="resetAllToDefaults()">
|
||||
<span>🔄</span>
|
||||
Reset All to Defaults
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveAllPermissions()">
|
||||
<span>💾</span>
|
||||
Save All Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize data from backend
|
||||
let permissions = {{ permissions_json|safe }};
|
||||
let rolePermissions = {{ role_permissions_json|safe }};
|
||||
|
||||
// Toggle permission function
|
||||
function togglePermission(roleName, pageKey, sectionKey, action, checkbox) {
|
||||
const isChecked = checkbox.checked;
|
||||
const permissionKey = `${pageKey}.${sectionKey}.${action}`;
|
||||
|
||||
// Update visual state of the function item
|
||||
const functionItem = checkbox.closest('.function-item');
|
||||
if (isChecked) {
|
||||
functionItem.classList.remove('disabled');
|
||||
} else {
|
||||
functionItem.classList.add('disabled');
|
||||
}
|
||||
|
||||
// Update data structure (flat array format)
|
||||
if (!rolePermissions[roleName]) {
|
||||
rolePermissions[roleName] = [];
|
||||
}
|
||||
|
||||
if (isChecked && !rolePermissions[roleName].includes(permissionKey)) {
|
||||
rolePermissions[roleName].push(permissionKey);
|
||||
} else if (!isChecked) {
|
||||
const index = rolePermissions[roleName].indexOf(permissionKey);
|
||||
if (index > -1) {
|
||||
rolePermissions[roleName].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save all permissions
|
||||
function saveAllPermissions() {
|
||||
// Convert flat permission arrays to nested structure for backend
|
||||
const structuredPermissions = {};
|
||||
|
||||
for (const [roleName, permissions] of Object.entries(rolePermissions)) {
|
||||
structuredPermissions[roleName] = {};
|
||||
|
||||
permissions.forEach(permissionKey => {
|
||||
const [pageKey, sectionKey, action] = permissionKey.split('.');
|
||||
|
||||
if (!structuredPermissions[roleName][pageKey]) {
|
||||
structuredPermissions[roleName][pageKey] = {};
|
||||
}
|
||||
if (!structuredPermissions[roleName][pageKey][sectionKey]) {
|
||||
structuredPermissions[roleName][pageKey][sectionKey] = [];
|
||||
}
|
||||
|
||||
structuredPermissions[roleName][pageKey][sectionKey].push(action);
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/settings/save_all_role_permissions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permissions: structuredPermissions
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('All permissions saved successfully!');
|
||||
} else {
|
||||
alert('Error saving permissions: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error saving permissions: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset all permissions to defaults
|
||||
function resetAllToDefaults() {
|
||||
if (confirm('Are you sure you want to reset ALL role permissions to defaults? This will overwrite all current settings.')) {
|
||||
fetch('/settings/reset_all_role_permissions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error resetting permissions: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error resetting permissions: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize checkbox states when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set initial states based on data
|
||||
document.querySelectorAll('.function-item').forEach(item => {
|
||||
const roleName = item.dataset.role;
|
||||
const permissionKey = item.dataset.permission;
|
||||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||||
|
||||
// Check if this role has this permission
|
||||
const hasPermission = rolePermissions[roleName] && rolePermissions[roleName].includes(permissionKey);
|
||||
|
||||
if (hasPermission) {
|
||||
checkbox.checked = true;
|
||||
item.classList.remove('disabled');
|
||||
} else {
|
||||
checkbox.checked = false;
|
||||
item.classList.add('disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -44,9 +44,7 @@
|
||||
<a href="{{ url_for('main.user_management_simple') }}" class="btn" style="background-color: #2196f3; color: white; margin-right: 10px;">
|
||||
🎯 Manage Users (Simplified)
|
||||
</a>
|
||||
<a href="{{ url_for('main.role_permissions') }}" class="btn" style="background-color: #6c757d; color: white;">
|
||||
⚙️ Advanced Role Permissions
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<small style="display: block; margin-top: 10px; color: #666;">
|
||||
Recommended: Use the simplified user management for easier administration
|
||||
|
||||
890
py_app/app/templates/user_management_simple.html
Normal file
890
py_app/app/templates/user_management_simple.html
Normal file
@@ -0,0 +1,890 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}User Management - Simplified{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Theme-aware card styles */
|
||||
.user-management-page .card {
|
||||
text-align: left !important;
|
||||
flex: none !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 5px !important;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Light mode card styles */
|
||||
body.light-mode .user-management-page .card {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .card-body {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .card-header {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #333 !important;
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
padding: 0.75rem 1.25rem !important;
|
||||
border-top-left-radius: 4px !important;
|
||||
border-top-right-radius: 4px !important;
|
||||
}
|
||||
|
||||
/* Dark mode card styles */
|
||||
body.dark-mode .user-management-page .card {
|
||||
background: #1e1e1e !important;
|
||||
color: #fff !important;
|
||||
border: 1px solid #444 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .card-body {
|
||||
background: #1e1e1e !important;
|
||||
color: #fff !important;
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .card-header {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ccc !important;
|
||||
border-bottom: 1px solid #444 !important;
|
||||
padding: 0.75rem 1.25rem !important;
|
||||
border-top-left-radius: 4px !important;
|
||||
border-top-right-radius: 4px !important;
|
||||
}
|
||||
|
||||
/* Theme-aware header text */
|
||||
.user-management-page .card-header h5 {
|
||||
margin: 0 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .card-header h5 {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .card-header h5 {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
/* Theme-aware role badges */
|
||||
.user-role-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.role-superadmin { background-color: #dc3545; color: white; }
|
||||
.role-admin { background-color: #fd7e14; color: white; }
|
||||
.role-manager { background-color: #007bff; color: white; }
|
||||
.role-worker { background-color: #28a745; color: white; }
|
||||
|
||||
/* Dark mode badge adjustments */
|
||||
body.dark-mode .role-manager { background-color: #0056b3; }
|
||||
body.dark-mode .role-worker { background-color: #1e7e34; }
|
||||
|
||||
/* Theme-aware module badges */
|
||||
.module-badges .badge {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
body.light-mode .module-badges .badge {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.dark-mode .module-badges .badge {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.module-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.module-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Theme-aware info panels */
|
||||
.access-level-info {
|
||||
border-left: 4px solid #007bff;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
body.light-mode .access-level-info {
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.dark-mode .access-level-info {
|
||||
background-color: #2a2a2a;
|
||||
color: #ccc;
|
||||
border-left-color: #0056b3;
|
||||
}
|
||||
|
||||
.table-warning {
|
||||
background-color: #fff3cd !important;
|
||||
}
|
||||
|
||||
body.dark-mode .table-warning {
|
||||
background-color: #664d03 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Colored card headers - respect theme but use accent colors */
|
||||
.card-header.bg-primary {
|
||||
background-color: #007bff !important;
|
||||
color: white !important;
|
||||
border-color: #007bff !important;
|
||||
}
|
||||
|
||||
.card-header.bg-primary h5 {
|
||||
color: white !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.card-header.bg-success {
|
||||
background-color: #28a745 !important;
|
||||
color: white !important;
|
||||
border-color: #28a745 !important;
|
||||
}
|
||||
|
||||
.card-header.bg-success h5 {
|
||||
color: white !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments for colored headers */
|
||||
body.dark-mode .card-header.bg-primary {
|
||||
background-color: #0056b3 !important;
|
||||
border-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .card-header.bg-success {
|
||||
background-color: #1e7e34 !important;
|
||||
border-color: #1e7e34 !important;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#quickModuleEdit {
|
||||
border: 2px solid #198754;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
background-color: #f8fff8;
|
||||
}
|
||||
|
||||
/* Theme-aware table styling */
|
||||
.thead-dark {
|
||||
background-color: #343a40 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
body.dark-mode .thead-dark {
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
.user-management-page .table {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .table {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .table {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.user-management-page .table th,
|
||||
.user-management-page .table td {
|
||||
border-top: 1px solid #dee2e6 !important;
|
||||
padding: 0.75rem !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .table th,
|
||||
body.dark-mode .user-management-page .table td {
|
||||
border-top: 1px solid #444 !important;
|
||||
}
|
||||
|
||||
.user-management-page .table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
.user-management-page .table-hover tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.075) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .table-hover tbody tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.075) !important;
|
||||
}
|
||||
|
||||
/* Theme-aware form elements */
|
||||
.user-management-page .form-control {
|
||||
border: 1px solid #ced4da !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .form-control {
|
||||
background-color: #fff !important;
|
||||
color: #000 !important;
|
||||
border-color: #ced4da !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .form-control {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #fff !important;
|
||||
border-color: #444 !important;
|
||||
}
|
||||
|
||||
.user-management-page .form-control:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .form-control:focus {
|
||||
border-color: #80bdff !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .form-control:focus {
|
||||
border-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
.user-management-page label {
|
||||
font-weight: 500 !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page label {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page label {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
/* Theme-aware buttons */
|
||||
.user-management-page .btn-primary {
|
||||
background-color: #007bff !important;
|
||||
border-color: #007bff !important;
|
||||
}
|
||||
|
||||
.user-management-page .btn-primary:hover {
|
||||
background-color: #0056b3 !important;
|
||||
border-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .btn-primary {
|
||||
background-color: #0056b3 !important;
|
||||
border-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .btn-primary:hover {
|
||||
background-color: #004085 !important;
|
||||
border-color: #004085 !important;
|
||||
}
|
||||
|
||||
/* Additional theme-aware elements */
|
||||
#quickModuleEdit {
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 2px solid #28a745;
|
||||
}
|
||||
|
||||
body.light-mode #quickModuleEdit {
|
||||
background-color: #f8fff8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.dark-mode #quickModuleEdit {
|
||||
background-color: #1a2f1a;
|
||||
color: #ccc;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
/* Theme-aware text colors */
|
||||
body.light-mode .user-management-page h2,
|
||||
body.light-mode .user-management-page p {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page h2,
|
||||
body.dark-mode .user-management-page p {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
body.light-mode .user-management-page .text-muted {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page .text-muted {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
|
||||
/* Theme-aware select dropdown */
|
||||
body.light-mode .user-management-page select.form-control {
|
||||
background-color: #fff !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .user-management-page select.form-control {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4 user-management-page">
|
||||
<h2>Simplified User Management</h2>
|
||||
<p class="text-muted">Manage users with the new 4-tier permission system: Superadmin → Admin → Manager → Worker</p>
|
||||
|
||||
<div class="row">
|
||||
<!-- Create User Card -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">👤 Create New User</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="addUserForm" method="POST" action="/create_user_simple">
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="role">Role:</label>
|
||||
<select id="role" name="role" class="form-control" required onchange="updateModuleSelection()">
|
||||
<option value="">Select a role...</option>
|
||||
<option value="superadmin">Superadmin - Full system access</option>
|
||||
<option value="admin">Admin - Full app access (except role permissions)</option>
|
||||
<option value="manager">Manager - Module-based access (can have multiple modules)</option>
|
||||
<option value="worker">Worker - Limited module access (can have multiple modules)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="moduleSelection" style="display: none;">
|
||||
<label>Modules:</label>
|
||||
<div class="module-checkboxes">
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="module_quality" name="modules" value="quality">
|
||||
<label for="module_quality">Quality Control</label>
|
||||
</div>
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="module_warehouse" name="modules" value="warehouse">
|
||||
<label for="module_warehouse">Warehouse Management</label>
|
||||
</div>
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="module_labels" name="modules" value="labels">
|
||||
<label for="module_labels">Label Management</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">Create User</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit User Rights Card -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">⚙️ Edit User Rights</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="userRightsPanel">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-user-edit fa-3x mb-3"></i>
|
||||
<p>Select a user from the table below to edit their rights and module access.</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Module Management for Selected User -->
|
||||
<div id="quickModuleEdit" style="display: none;">
|
||||
<h6>Quick Module Management</h6>
|
||||
<div class="mb-3">
|
||||
<strong>User:</strong> <span id="selectedUserName"></span>
|
||||
<br><small class="text-muted">Role: <span id="selectedUserRole"></span></small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Current Modules:</label>
|
||||
<div id="currentModules" class="module-checkboxes">
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="quick_module_quality" name="quick_modules" value="quality">
|
||||
<label for="quick_module_quality">Quality Control</label>
|
||||
</div>
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="quick_module_warehouse" name="quick_modules" value="warehouse">
|
||||
<label for="quick_module_warehouse">Warehouse Management</label>
|
||||
</div>
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="quick_module_labels" name="quick_modules" value="labels">
|
||||
<label for="quick_module_labels">Label Management</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm w-100">
|
||||
<button type="button" class="btn btn-success" onclick="updateUserModules()">
|
||||
💾 Save Module Changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-info" onclick="showFullEditModal()">
|
||||
✏️ Full Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="deleteSelectedUser()">
|
||||
🗑️ Delete User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Users Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>👥 Current Users</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if users %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Modules</th>
|
||||
<th>Access Level</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr class="user-row" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-role="{{ user.role }}" data-modules="{{ (user.get_modules() or []) | tojson }}">
|
||||
<td>
|
||||
<strong>{{ user.username }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="user-role-badge role-{{ user.role }}">
|
||||
{{ user.role.title() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="module-badges">
|
||||
{% if user.role in ['superadmin', 'admin'] %}
|
||||
<span class="badge bg-secondary">All Modules</span>
|
||||
{% elif user.modules %}
|
||||
{% for module in user.get_modules() %}
|
||||
<span class="badge bg-info">{{ module.title() }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">No modules assigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{% if user.role == 'superadmin' %}
|
||||
Full system access
|
||||
{% elif user.role == 'admin' %}
|
||||
Full app access
|
||||
{% elif user.role == 'manager' %}
|
||||
Full module access + reports
|
||||
{% elif user.role == 'worker' %}
|
||||
Basic operations only (no reports) - Can have multiple modules
|
||||
{% endif %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary select-user-btn"
|
||||
data-user-id="{{ user.id }}"
|
||||
data-username="{{ user.username }}"
|
||||
data-role="{{ user.role }}"
|
||||
data-modules="{{ (user.get_modules() or []) | tojson }}"
|
||||
title="Select for quick edit">
|
||||
📝 Select
|
||||
</button>
|
||||
<button class="btn btn-outline-info edit-user-btn"
|
||||
data-user-id="{{ user.id }}"
|
||||
data-username="{{ user.username }}"
|
||||
data-role="{{ user.role }}"
|
||||
data-modules="{{ (user.get_modules() or []) | tojson }}"
|
||||
title="Full edit">
|
||||
⚙️ Edit
|
||||
</button>
|
||||
{% if user.username != session.get('user') %}
|
||||
<button class="btn btn-outline-danger delete-user-btn"
|
||||
data-user-id="{{ user.id }}"
|
||||
data-username="{{ user.username }}"
|
||||
title="Delete user">
|
||||
🗑️
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-users fa-3x mb-3"></i>
|
||||
<p>No users found. Create your first user above!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit User Modal -->
|
||||
<div class="modal fade" id="editUserModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit User</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="editUserForm" method="POST" action="/edit_user_simple">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit_user_id" name="user_id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_username">Username:</label>
|
||||
<input type="text" id="edit_username" name="username" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_password">Password (leave blank to keep current):</label>
|
||||
<input type="password" id="edit_password" name="password" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_role">Role:</label>
|
||||
<select id="edit_role" name="role" class="form-control" required onchange="updateEditModuleSelection()">
|
||||
<option value="">Select a role...</option>
|
||||
<option value="superadmin">Superadmin - Full system access</option>
|
||||
<option value="admin">Admin - Full app access (except role permissions)</option>
|
||||
<option value="manager">Manager - Module-based access (can have multiple modules)</option>
|
||||
<option value="worker">Worker - Limited module access (can have multiple modules)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="editModuleSelection" style="display: none;">
|
||||
<label>Modules:</label>
|
||||
<div class="module-checkboxes">
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="edit_module_quality" name="modules" value="quality">
|
||||
<label for="edit_module_quality">Quality Control</label>
|
||||
</div>
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="edit_module_warehouse" name="modules" value="warehouse">
|
||||
<label for="edit_module_warehouse">Warehouse Management</label>
|
||||
</div>
|
||||
<div class="module-checkbox">
|
||||
<input type="checkbox" id="edit_module_labels" name="modules" value="labels">
|
||||
<label for="edit_module_labels">Label Management</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateModuleSelection() {
|
||||
const role = document.getElementById('role').value;
|
||||
const moduleSelection = document.getElementById('moduleSelection');
|
||||
const accessLevelInfo = document.getElementById('accessLevelInfo');
|
||||
const checkboxes = document.querySelectorAll('#moduleSelection input[type="checkbox"]');
|
||||
|
||||
if (role === 'superadmin' || role === 'admin') {
|
||||
moduleSelection.style.display = 'none';
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
accessLevelInfo.style.display = 'none';
|
||||
} else if (role === 'manager' || role === 'worker') {
|
||||
moduleSelection.style.display = 'block';
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
|
||||
if (role === 'manager') {
|
||||
accessLevelInfo.style.display = 'block';
|
||||
accessLevelInfo.innerHTML = '<strong>Manager Access:</strong> Can have multiple modules. Full access to assigned modules including reports and management functions.';
|
||||
} else if (role === 'worker') {
|
||||
accessLevelInfo.style.display = 'block';
|
||||
accessLevelInfo.innerHTML = '<strong>Worker Access:</strong> Can have multiple modules. Limited to basic operations only (no reports or management functions). <br><small>Examples: Quality worker can scan, Warehouse worker can move orders, etc.</small>';
|
||||
}
|
||||
} else {
|
||||
moduleSelection.style.display = 'none';
|
||||
accessLevelInfo.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateEditModuleSelection() {
|
||||
const role = document.getElementById('edit_role').value;
|
||||
const moduleSelection = document.getElementById('editModuleSelection');
|
||||
const accessLevelInfo = document.getElementById('editAccessLevelInfo');
|
||||
const checkboxes = document.querySelectorAll('#editModuleSelection input[type="checkbox"]');
|
||||
|
||||
if (role === 'superadmin' || role === 'admin') {
|
||||
moduleSelection.style.display = 'none';
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
accessLevelInfo.style.display = 'none';
|
||||
} else if (role === 'manager' || role === 'worker') {
|
||||
moduleSelection.style.display = 'block';
|
||||
|
||||
if (role === 'manager') {
|
||||
accessLevelInfo.style.display = 'block';
|
||||
accessLevelInfo.innerHTML = '<strong>Manager Access:</strong> Can have multiple modules. Full access to assigned modules including reports and management functions.';
|
||||
} else if (role === 'worker') {
|
||||
accessLevelInfo.style.display = 'block';
|
||||
accessLevelInfo.innerHTML = '<strong>Worker Access:</strong> Can have multiple modules. Limited to basic operations only (no reports or management functions). <br><small>Examples: Quality worker can scan, Warehouse worker can move orders, etc.</small>';
|
||||
}
|
||||
} else {
|
||||
moduleSelection.style.display = 'none';
|
||||
accessLevelInfo.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to store selected user
|
||||
let selectedUser = null;
|
||||
|
||||
function selectUserForQuickEdit(userId, username, role, modules) {
|
||||
selectedUser = {id: userId, username: username, role: role, modules: modules};
|
||||
|
||||
// Update the quick edit panel
|
||||
document.getElementById('selectedUserName').textContent = username;
|
||||
document.getElementById('selectedUserRole').textContent = role;
|
||||
|
||||
// Clear all module checkboxes first
|
||||
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
|
||||
// Check the appropriate modules
|
||||
if (modules && Array.isArray(modules)) {
|
||||
modules.forEach(module => {
|
||||
const checkbox = document.getElementById('quick_module_' + module);
|
||||
if (checkbox) checkbox.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Show the quick edit panel
|
||||
document.getElementById('quickModuleEdit').style.display = 'block';
|
||||
document.querySelector('#userRightsPanel .text-center').style.display = 'none';
|
||||
|
||||
// Highlight selected row
|
||||
document.querySelectorAll('.user-row').forEach(row => {
|
||||
row.classList.remove('table-warning');
|
||||
});
|
||||
document.querySelector(`[data-user-id="${userId}"]`).classList.add('table-warning');
|
||||
}
|
||||
|
||||
function updateUserModules() {
|
||||
if (!selectedUser) {
|
||||
alert('No user selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected modules
|
||||
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]:checked');
|
||||
const modules = Array.from(checkboxes).map(cb => cb.value);
|
||||
|
||||
// Validate modules for the role
|
||||
if ((selectedUser.role === 'manager' || selectedUser.role === 'worker') && modules.length === 0) {
|
||||
alert(`${selectedUser.role}s must have at least one module assigned.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and submit form
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/quick_update_modules';
|
||||
|
||||
// Add user ID
|
||||
const userIdInput = document.createElement('input');
|
||||
userIdInput.type = 'hidden';
|
||||
userIdInput.name = 'user_id';
|
||||
userIdInput.value = selectedUser.id;
|
||||
form.appendChild(userIdInput);
|
||||
|
||||
// Add modules
|
||||
modules.forEach(module => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'modules';
|
||||
input.value = module;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function showFullEditModal() {
|
||||
if (!selectedUser) {
|
||||
alert('No user selected');
|
||||
return;
|
||||
}
|
||||
|
||||
editUser(selectedUser.id, selectedUser.username, selectedUser.role, selectedUser.modules);
|
||||
}
|
||||
|
||||
function deleteSelectedUser() {
|
||||
if (!selectedUser) {
|
||||
alert('No user selected');
|
||||
return;
|
||||
}
|
||||
|
||||
deleteUser(selectedUser.id, selectedUser.username);
|
||||
}
|
||||
|
||||
function editUser(userId, username, role, modules) {
|
||||
document.getElementById('edit_user_id').value = userId;
|
||||
document.getElementById('edit_username').value = username;
|
||||
document.getElementById('edit_role').value = role;
|
||||
document.getElementById('edit_password').value = '';
|
||||
|
||||
// Clear all module checkboxes first
|
||||
const checkboxes = document.querySelectorAll('#editModuleSelection input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
|
||||
// Check the appropriate modules
|
||||
if (modules && Array.isArray(modules)) {
|
||||
modules.forEach(module => {
|
||||
const checkbox = document.getElementById('edit_module_' + module);
|
||||
if (checkbox) checkbox.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
updateEditModuleSelection();
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function deleteUser(userId, username) {
|
||||
if (confirm(`Are you sure you want to delete user "${username}"?`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/delete_user_simple';
|
||||
|
||||
const userIdInput = document.createElement('input');
|
||||
userIdInput.type = 'hidden';
|
||||
userIdInput.name = 'user_id';
|
||||
userIdInput.value = userId;
|
||||
|
||||
form.appendChild(userIdInput);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteUser(userId, username) {
|
||||
if (confirm(`Are you sure you want to delete user "${username}"?`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/delete_user_simple';
|
||||
|
||||
const userIdInput = document.createElement('input');
|
||||
userIdInput.type = 'hidden';
|
||||
userIdInput.name = 'user_id';
|
||||
userIdInput.value = userId;
|
||||
|
||||
form.appendChild(userIdInput);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateModuleSelection();
|
||||
|
||||
// Add event listeners for select user buttons
|
||||
document.querySelectorAll('.select-user-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
const username = this.dataset.username;
|
||||
const role = this.dataset.role;
|
||||
let modules = [];
|
||||
try {
|
||||
modules = JSON.parse(this.dataset.modules) || [];
|
||||
} catch (e) {
|
||||
console.log('Error parsing modules for user:', username, e);
|
||||
modules = [];
|
||||
}
|
||||
selectUserForQuickEdit(userId, username, role, modules);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for edit buttons
|
||||
document.querySelectorAll('.edit-user-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
const username = this.dataset.username;
|
||||
const role = this.dataset.role;
|
||||
let modules = [];
|
||||
try {
|
||||
modules = JSON.parse(this.dataset.modules) || [];
|
||||
} catch (e) {
|
||||
console.log('Error parsing modules for user:', username, e);
|
||||
modules = [];
|
||||
}
|
||||
editUser(userId, username, role, modules);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for delete buttons
|
||||
document.querySelectorAll('.delete-user-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
const username = this.dataset.username;
|
||||
deleteUser(userId, username);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user