diff --git a/app/access_control.py b/app/access_control.py
index 963becd..2daaf81 100644
--- a/app/access_control.py
+++ b/app/access_control.py
@@ -11,25 +11,37 @@ ROLES = {
'name': 'Super Administrator',
'description': 'Full system access to all modules and features',
'level': 100,
- 'modules': ['quality', 'settings']
- },
- 'manager': {
- 'name': 'Manager',
- 'description': 'Full access to assigned modules and quality control',
- 'level': 70,
- 'modules': ['quality']
- },
- 'worker': {
- 'name': 'Worker',
- 'description': 'Limited access to view and create quality inspections',
- 'level': 50,
- 'modules': ['quality']
+ 'modules': ['quality', 'settings', 'warehouse']
},
'admin': {
'name': 'Administrator',
'description': 'Administrative access - can manage users and system configuration',
'level': 90,
- 'modules': ['quality', 'settings']
+ 'modules': ['quality', 'settings', 'warehouse']
+ },
+ 'manager': {
+ 'name': 'Manager - Quality',
+ 'description': 'Full access to quality module and quality control',
+ 'level': 70,
+ 'modules': ['quality']
+ },
+ 'warehouse_manager': {
+ 'name': 'Manager - Warehouse',
+ 'description': 'Full access to warehouse module - input and reports',
+ 'level': 75,
+ 'modules': ['warehouse']
+ },
+ 'worker': {
+ 'name': 'Worker - Quality',
+ 'description': 'Limited access to quality inspections - input only',
+ 'level': 50,
+ 'modules': ['quality']
+ },
+ 'warehouse_worker': {
+ 'name': 'Worker - Warehouse',
+ 'description': 'Limited access to warehouse - input pages only, no reports',
+ 'level': 35,
+ 'modules': ['warehouse']
}
}
@@ -101,7 +113,72 @@ MODULE_PERMISSIONS = {
'superadmin': ['view', 'edit'],
'admin': ['view', 'edit'],
'manager': [],
- 'worker': []
+ 'worker': [],
+ 'warehouse_manager': [],
+ 'warehouse_worker': []
+ }
+ }
+ },
+ 'warehouse': {
+ 'name': 'Warehouse Module',
+ 'sections': {
+ 'input': {
+ 'name': 'Warehouse Data Input',
+ 'actions': {
+ 'view': 'View warehouse input pages',
+ 'create': 'Create warehouse entries',
+ 'edit': 'Edit warehouse entries',
+ 'delete': 'Delete warehouse entries'
+ },
+ 'superadmin': ['view', 'create', 'edit', 'delete'],
+ 'admin': ['view', 'create', 'edit', 'delete'],
+ 'manager': [],
+ 'worker': [],
+ 'warehouse_manager': ['view', 'create', 'edit', 'delete'],
+ 'warehouse_worker': ['view', 'create', 'edit']
+ },
+ 'reports': {
+ 'name': 'Warehouse Reports & Analytics',
+ 'actions': {
+ 'view': 'View warehouse reports',
+ 'export': 'Export warehouse data',
+ 'download': 'Download reports',
+ 'analytics': 'View analytics'
+ },
+ 'superadmin': ['view', 'export', 'download', 'analytics'],
+ 'admin': ['view', 'export', 'download', 'analytics'],
+ 'manager': [],
+ 'worker': [],
+ 'warehouse_manager': ['view', 'export', 'download', 'analytics'],
+ 'warehouse_worker': []
+ },
+ 'locations': {
+ 'name': 'Location Management',
+ 'actions': {
+ 'view': 'View locations',
+ 'create': 'Create locations',
+ 'edit': 'Edit locations',
+ 'delete': 'Delete locations'
+ },
+ 'superadmin': ['view', 'create', 'edit', 'delete'],
+ 'admin': ['view', 'create', 'edit', 'delete'],
+ 'manager': [],
+ 'worker': [],
+ 'warehouse_manager': ['view', 'create', 'edit', 'delete'],
+ 'warehouse_worker': ['view']
+ },
+ 'management': {
+ 'name': 'Warehouse User Management',
+ 'actions': {
+ 'manage_workers': 'Manage assigned workers',
+ 'manage_zones': 'Manage warehouse zones'
+ },
+ 'superadmin': ['manage_workers', 'manage_zones'],
+ 'admin': ['manage_workers', 'manage_zones'],
+ 'manager': [],
+ 'worker': [],
+ 'warehouse_manager': ['manage_workers'],
+ 'warehouse_worker': []
}
}
}
@@ -277,3 +354,192 @@ def get_user_permissions(user_role):
permissions[module][section] = allowed_actions
return permissions
+
+
+# Warehouse-specific access control helpers
+
+def can_access_warehouse_input(user_role):
+ """
+ Check if user can access warehouse INPUT pages
+
+ Args:
+ user_role (str): User's role
+
+ Returns:
+ bool: True if user can access input pages (managers and workers)
+ """
+ return check_permission(user_role, 'warehouse', 'input', 'view')
+
+
+def can_access_warehouse_reports(user_role):
+ """
+ Check if user can access warehouse REPORT/ANALYTICS pages
+
+ Args:
+ user_role (str): User's role
+
+ Returns:
+ bool: True if user can access reports (managers only)
+ """
+ return check_permission(user_role, 'warehouse', 'reports', 'view')
+
+
+def can_manage_warehouse_workers(user_role):
+ """
+ Check if user can manage warehouse workers (assign/unassign)
+
+ Args:
+ user_role (str): User's role
+
+ Returns:
+ bool: True if user can manage workers (managers only)
+ """
+ return check_permission(user_role, 'warehouse', 'management', 'manage_workers')
+
+
+# Zone-restricted access functions
+
+def get_worker_warehouse_zone(user_id):
+ """
+ Get the warehouse zone restriction for a warehouse_worker
+
+ Args:
+ user_id (int): Worker user ID
+
+ Returns:
+ str or None: Zone name if restricted, None if all zones allowed
+ """
+ try:
+ from app.database import get_db
+ db = get_db()
+ cursor = db.cursor()
+
+ cursor.execute("""
+ SELECT warehouse_zone FROM worker_manager_bindings
+ WHERE worker_id = %s AND is_active = 1
+ LIMIT 1
+ """, (user_id,))
+
+ result = cursor.fetchone()
+ if result:
+ return result[0] # Return zone name or None
+ return None
+ except Exception as e:
+ logger.error(f"Error getting worker zone: {e}")
+ return None
+
+
+def get_manager_workers(manager_id):
+ """
+ Get all active workers assigned to a manager
+
+ Args:
+ manager_id (int): Manager user ID
+
+ Returns:
+ list: List of dicts with worker info {id, username, full_name, zone}
+ """
+ try:
+ from app.database import get_db
+ db = get_db()
+ cursor = db.cursor()
+
+ cursor.execute("""
+ SELECT u.id, u.username, u.full_name, wmb.warehouse_zone
+ FROM worker_manager_bindings wmb
+ JOIN users u ON wmb.worker_id = u.id
+ WHERE wmb.manager_id = %s AND wmb.is_active = 1
+ ORDER BY u.full_name
+ """, (manager_id,))
+
+ results = []
+ for row in cursor.fetchall():
+ results.append({
+ 'id': row[0],
+ 'username': row[1],
+ 'full_name': row[2],
+ 'zone': row[3]
+ })
+ return results
+ except Exception as e:
+ logger.error(f"Error getting manager workers: {e}")
+ return []
+
+
+def validate_worker_zone_access(worker_id, manager_id, zone):
+ """
+ Validate that a worker can access a specific zone
+
+ Args:
+ worker_id (int): Worker user ID
+ manager_id (int): Expected manager ID
+ zone (str): Zone name to validate
+
+ Returns:
+ bool: True if worker can access zone
+ """
+ try:
+ from app.database import get_db
+ db = get_db()
+ cursor = db.cursor()
+
+ cursor.execute("""
+ SELECT warehouse_zone FROM worker_manager_bindings
+ WHERE worker_id = %s AND manager_id = %s AND is_active = 1
+ LIMIT 1
+ """, (worker_id, manager_id))
+
+ result = cursor.fetchone()
+ if not result:
+ return False # Binding doesn't exist
+
+ assigned_zone = result[0]
+
+ # If no zone restriction (NULL), worker can access all zones
+ if assigned_zone is None:
+ return True
+
+ # If zone is restricted, must match the requested zone
+ return assigned_zone == zone
+ except Exception as e:
+ logger.error(f"Error validating worker zone access: {e}")
+ return False
+
+
+def build_zone_filter_sql(user_id, user_role):
+ """
+ Build WHERE clause SQL fragment for zone-filtered queries
+
+ For managers: returns all data from assigned workers
+ For workers: returns only data from their assigned zone
+ For others: returns no data or all data depending on role
+
+ Args:
+ user_id (int): User ID
+ user_role (str): User's role
+
+ Returns:
+ str: SQL WHERE fragment (empty string if no filter needed)
+ """
+ if user_role in ['superadmin', 'admin']:
+ return "" # No filter - see everything
+
+ if user_role == 'warehouse_manager':
+ # Manager sees data from all assigned workers
+ try:
+ workers = get_manager_workers(user_id)
+ if not workers:
+ # Manager has no workers assigned - see own data only
+ return f"AND created_by_user_id = {user_id}"
+ worker_ids = [w['id'] for w in workers]
+ return f"AND (created_by_user_id IN ({','.join(map(str, worker_ids))}) OR created_by_user_id = {user_id})"
+ except Exception as e:
+ logger.error(f"Error building manager zone filter: {e}")
+ return f"AND created_by_user_id = {user_id}"
+
+ if user_role == 'warehouse_worker':
+ # Worker sees only their own data in their assigned zone
+ return f"AND created_by_user_id = {user_id}"
+
+ return "" # Default - no filtering
+
diff --git a/app/db_migrations/add_warehouse_roles_and_bindings.sql b/app/db_migrations/add_warehouse_roles_and_bindings.sql
new file mode 100644
index 0000000..e8cb653
--- /dev/null
+++ b/app/db_migrations/add_warehouse_roles_and_bindings.sql
@@ -0,0 +1,80 @@
+-- Warehouse Module Roles & Worker-Manager Binding Migration
+-- This migration adds:
+-- 1. Two new warehouse roles (warehouse_manager, warehouse_worker)
+-- 2. Worker-manager binding table for hierarchical access control
+
+-- ============================================================
+-- 1. Insert new warehouse roles
+-- ============================================================
+
+-- Note: These roles should be inserted if they don't exist
+-- This is typically handled by the Python init script, but this
+-- SQL is provided for reference or manual database setup
+
+INSERT IGNORE INTO roles (name, description, level) VALUES
+('warehouse_manager', 'Manager - Warehouse - Full warehouse module access', 75),
+('warehouse_worker', 'Worker - Warehouse - Input-only warehouse access', 35);
+
+-- Verify insertion
+SELECT id, name, level FROM roles WHERE name LIKE 'warehouse_%' ORDER BY level DESC;
+
+-- ============================================================
+-- 2. Create worker_manager_bindings table
+-- ============================================================
+
+CREATE TABLE IF NOT EXISTS worker_manager_bindings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ manager_id INT NOT NULL,
+ worker_id INT NOT NULL,
+ warehouse_zone VARCHAR(100),
+ is_active TINYINT(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY unique_binding (manager_id, worker_id),
+ FOREIGN KEY (manager_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (worker_id) REFERENCES users(id) ON DELETE CASCADE,
+ CHECK (manager_id != worker_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- ============================================================
+-- 3. Example queries for worker-manager binding management
+-- ============================================================
+
+-- Assign a worker to a manager
+-- INSERT INTO worker_manager_bindings (manager_id, worker_id, warehouse_zone)
+-- VALUES (5, 12, NULL); -- Manager ID 5 oversees Worker ID 12
+
+-- Get all workers for a specific manager
+-- SELECT u.id, u.username, u.full_name, wmb.warehouse_zone
+-- FROM worker_manager_bindings wmb
+-- JOIN users u ON wmb.worker_id = u.id
+-- WHERE wmb.manager_id = 5 AND wmb.is_active = 1
+-- ORDER BY u.full_name;
+
+-- Get manager for a specific worker
+-- SELECT m.id, m.username, m.full_name
+-- FROM worker_manager_bindings wmb
+-- JOIN users m ON wmb.manager_id = m.id
+-- WHERE wmb.worker_id = 12 AND wmb.is_active = 1;
+
+-- Deactivate a binding (soft delete)
+-- UPDATE worker_manager_bindings SET is_active = 0 WHERE id = 1;
+
+-- ============================================================
+-- 4. Verification Queries
+-- ============================================================
+
+-- Verify warehouse roles exist
+-- SELECT COUNT(*) as warehouse_role_count FROM roles WHERE name LIKE 'warehouse_%';
+
+-- Verify worker_manager_bindings table exists
+-- SELECT TABLE_NAME FROM information_schema.TABLES
+-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'worker_manager_bindings';
+
+-- List all role hierarchy (for reference)
+-- SELECT name, level, CASE
+-- WHEN level >= 90 THEN 'Admin'
+-- WHEN level >= 70 THEN 'Manager'
+-- WHEN level >= 35 THEN 'Worker'
+-- ELSE 'Unknown'
+-- END as category FROM roles ORDER BY level DESC;
diff --git a/app/db_schema_verifier.py b/app/db_schema_verifier.py
new file mode 100644
index 0000000..7847a3f
--- /dev/null
+++ b/app/db_schema_verifier.py
@@ -0,0 +1,373 @@
+"""
+Database Schema Verification & Repair Module
+Checks existing database structure and adds missing tables/columns/data
+Safely handles both fresh databases and existing installations
+"""
+import pymysql
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class SchemaVerifier:
+ """Verify and repair database schema structure"""
+
+ def __init__(self, connection):
+ self.conn = connection
+ self.cursor = connection.cursor()
+ self.changes_made = []
+
+ def verify_and_repair(self):
+ """
+ Main verification and repair process
+
+ Returns:
+ tuple: (success: bool, changes_summary: str)
+ """
+ try:
+ logger.info("=" * 60)
+ logger.info("Starting database schema verification...")
+ logger.info("=" * 60)
+
+ # 1. Verify tables
+ self.verify_tables()
+
+ # 2. Verify columns in existing tables
+ self.verify_columns()
+
+ # 3. Verify reference data (roles, etc)
+ self.verify_reference_data()
+
+ # Commit all changes
+ self.conn.commit()
+
+ # Generate summary
+ summary = self.generate_summary()
+ logger.info("=" * 60)
+ logger.info("✓ Database schema verification complete")
+ logger.info("=" * 60)
+
+ return True, summary
+
+ except Exception as e:
+ logger.error(f"✗ Schema verification failed: {e}")
+ self.conn.rollback()
+ return False, f"Error: {str(e)}"
+
+ def table_exists(self, table_name):
+ """Check if table exists in database"""
+ try:
+ self.cursor.execute(f"""
+ SELECT COUNT(*) FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s
+ """, (table_name,))
+ return self.cursor.fetchone()[0] > 0
+ except Exception as e:
+ logger.error(f"Error checking if table {table_name} exists: {e}")
+ return False
+
+ def column_exists(self, table_name, column_name):
+ """Check if column exists in table"""
+ try:
+ self.cursor.execute(f"""
+ SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = %s
+ AND COLUMN_NAME = %s
+ """, (table_name, column_name))
+ return self.cursor.fetchone()[0] > 0
+ except Exception as e:
+ logger.error(f"Error checking column {table_name}.{column_name}: {e}")
+ return False
+
+ def get_table_columns(self, table_name):
+ """Get all columns for a table"""
+ try:
+ self.cursor.execute(f"""
+ SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s
+ """, (table_name,))
+ return {row[0]: row for row in self.cursor.fetchall()}
+ except Exception as e:
+ logger.error(f"Error getting columns for {table_name}: {e}")
+ return {}
+
+ def verify_tables(self):
+ """Verify all required tables exist"""
+ logger.info("Verifying tables...")
+
+ # Define all required tables with their schemas
+ tables_to_verify = {
+ 'users': self.get_users_schema,
+ 'user_credentials': self.get_user_credentials_schema,
+ 'user_modules': self.get_user_modules_schema,
+ 'user_permissions': self.get_user_permissions_schema,
+ 'roles': self.get_roles_schema,
+ 'worker_manager_bindings': self.get_worker_manager_bindings_schema,
+ 'application_settings': self.get_application_settings_schema,
+ 'audit_logs': self.get_audit_logs_schema,
+ 'backup_schedules': self.get_backup_schedules_schema,
+ }
+
+ for table_name, schema_getter in tables_to_verify.items():
+ if not self.table_exists(table_name):
+ logger.info(f" ⚠ Table '{table_name}' missing - creating...")
+ try:
+ sql = schema_getter()
+ self.cursor.execute(sql)
+ self.changes_made.append(f"Created table '{table_name}'")
+ logger.info(f" ✓ Created table '{table_name}'")
+ except Exception as e:
+ logger.error(f" ✗ Failed to create table '{table_name}': {e}")
+ raise
+ else:
+ logger.info(f" ✓ Table '{table_name}' exists")
+
+ def verify_columns(self):
+ """Verify all columns exist in tables"""
+ logger.info("Verifying table columns...")
+
+ # Define required columns for each table
+ columns_to_verify = {
+ 'users': [
+ ('id', 'INT', 'NO', 'PRI', 'auto_increment'),
+ ('username', 'VARCHAR(100)', 'NO', 'UNI', ''),
+ ('email', 'VARCHAR(255)', 'YES', 'UNI', ''),
+ ('full_name', 'VARCHAR(255)', 'YES', '', ''),
+ ('role', 'VARCHAR(100)', 'YES', 'MUL', ''),
+ ('is_active', 'TINYINT(1)', 'YES', '', ''),
+ ('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
+ ('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
+ ],
+ 'worker_manager_bindings': [
+ ('id', 'INT', 'NO', 'PRI', 'auto_increment'),
+ ('manager_id', 'INT', 'NO', 'MUL', ''),
+ ('worker_id', 'INT', 'NO', '', ''),
+ ('warehouse_zone', 'VARCHAR(100)', 'YES', '', ''),
+ ('is_active', 'TINYINT(1)', 'YES', '', ''),
+ ('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
+ ('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
+ ],
+ 'roles': [
+ ('id', 'INT', 'NO', 'PRI', 'auto_increment'),
+ ('name', 'VARCHAR(100)', 'NO', 'UNI', ''),
+ ('description', 'TEXT', 'YES', '', ''),
+ ('level', 'INT', 'YES', '', ''),
+ ('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
+ ('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
+ ],
+ }
+
+ for table_name, required_columns in columns_to_verify.items():
+ if not self.table_exists(table_name):
+ continue
+
+ logger.info(f" Checking columns in '{table_name}'...")
+ existing_columns = self.get_table_columns(table_name)
+
+ for col_name, col_type, nullable, key, extra in required_columns:
+ if col_name not in existing_columns:
+ logger.info(f" ⚠ Column '{col_name}' missing - adding...")
+ try:
+ self.add_column(table_name, col_name, col_type, nullable)
+ self.changes_made.append(f"Added column '{table_name}.{col_name}'")
+ logger.info(f" ✓ Added column '{col_name}' to '{table_name}'")
+ except Exception as e:
+ logger.error(f" ✗ Failed to add column: {e}")
+ raise
+ else:
+ logger.info(f" ✓ Column '{col_name}' exists")
+
+ def add_column(self, table_name, column_name, column_type, nullable='YES'):
+ """Add a missing column to a table"""
+ null_clause = 'NULL' if nullable == 'YES' else 'NOT NULL'
+ sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type} {null_clause}"
+ self.cursor.execute(sql)
+
+ def verify_reference_data(self):
+ """Verify and add missing reference data (roles, etc)"""
+ logger.info("Verifying reference data...")
+
+ if not self.table_exists('roles'):
+ logger.info(" ⚠ Roles table doesn't exist, skipping reference data")
+ return
+
+ # Define all required roles
+ required_roles = [
+ ('superadmin', 'Super Administrator - Full system access', 100),
+ ('admin', 'Administrator - Administrative access', 90),
+ ('manager', 'Manager - Quality - Full access to assigned modules', 70),
+ ('warehouse_manager', 'Manager - Warehouse - Full warehouse module access', 75),
+ ('worker', 'Worker - Quality - Limited access', 50),
+ ('warehouse_worker', 'Worker - Warehouse - Input-only warehouse access', 35),
+ ]
+
+ logger.info(" Verifying roles...")
+ for role_name, role_desc, role_level in required_roles:
+ self.cursor.execute("SELECT id FROM roles WHERE name = %s", (role_name,))
+
+ if not self.cursor.fetchone():
+ try:
+ self.cursor.execute(
+ "INSERT INTO roles (name, description, level) VALUES (%s, %s, %s)",
+ (role_name, role_desc, role_level)
+ )
+ self.changes_made.append(f"Added role '{role_name}'")
+ logger.info(f" ✓ Added role '{role_name}'")
+ except Exception as e:
+ logger.error(f" ✗ Failed to add role '{role_name}': {e}")
+ else:
+ logger.info(f" ✓ Role '{role_name}' exists")
+
+ def generate_summary(self):
+ """Generate a summary of changes made"""
+ if not self.changes_made:
+ summary = "✓ Database structure is correct - no changes needed"
+ else:
+ summary = f"✓ Database repair complete - {len(self.changes_made)} change(s) made:\n"
+ for i, change in enumerate(self.changes_made, 1):
+ summary += f" {i}. {change}\n"
+
+ return summary
+
+ # Schema definitions
+ @staticmethod
+ def get_users_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(100) UNIQUE NOT NULL,
+ email VARCHAR(255) UNIQUE,
+ full_name VARCHAR(255),
+ role VARCHAR(100),
+ is_active TINYINT(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_role (role),
+ INDEX idx_username (username),
+ FOREIGN KEY (role) REFERENCES roles(name)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_user_credentials_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS user_credentials (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL UNIQUE,
+ password_hash VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_roles_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS roles (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(100) UNIQUE NOT NULL,
+ description TEXT,
+ level INT DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_user_modules_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS user_modules (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ module_name VARCHAR(100) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY unique_user_module (user_id, module_name),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_user_permissions_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS user_permissions (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ module_name VARCHAR(100) NOT NULL,
+ section_name VARCHAR(100) NOT NULL,
+ action_name VARCHAR(100) NOT NULL,
+ granted TINYINT(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY unique_permission (user_id, module_name, section_name, action_name),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_worker_manager_bindings_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS worker_manager_bindings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ manager_id INT NOT NULL,
+ worker_id INT NOT NULL,
+ warehouse_zone VARCHAR(100),
+ is_active TINYINT(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY unique_binding (manager_id, worker_id),
+ FOREIGN KEY (manager_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (worker_id) REFERENCES users(id) ON DELETE CASCADE,
+ CHECK (manager_id != worker_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_application_settings_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS application_settings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ setting_key VARCHAR(255) UNIQUE NOT NULL,
+ setting_value LONGTEXT,
+ setting_type VARCHAR(50),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_audit_logs_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS audit_logs (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT,
+ action VARCHAR(255) NOT NULL,
+ entity_type VARCHAR(100),
+ entity_id INT,
+ details JSON,
+ ip_address VARCHAR(45),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_user_id (user_id),
+ INDEX idx_action (action),
+ INDEX idx_created_at (created_at),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
+
+ @staticmethod
+ def get_backup_schedules_schema():
+ return """
+ CREATE TABLE IF NOT EXISTS backup_schedules (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ schedule_name VARCHAR(255) NOT NULL,
+ frequency VARCHAR(50) NOT NULL,
+ last_backup TIMESTAMP,
+ next_backup TIMESTAMP,
+ retention_days INT DEFAULT 30,
+ is_active TINYINT(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ """
diff --git a/app/modules/quality/routes.py b/app/modules/quality/routes.py
index ee4a15f..7bc8903 100644
--- a/app/modules/quality/routes.py
+++ b/app/modules/quality/routes.py
@@ -63,15 +63,11 @@ def fg_scan():
operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time
)
- # Flash appropriate message based on defect code
- if int(defect_code) == 0 or defect_code == '000':
- flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
- else:
- flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
+ # Note: Flash messages are NOT used here - JS notifications handle this on the frontend
+ # This prevents wide flash message alerts from appearing
except Exception as e:
logger.error(f"Error saving finish goods scan data: {e}")
- flash(f"Error saving scan data: {str(e)}", 'error')
# Check if this is an AJAX request (for scan-to-boxes feature)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.accept_mimetypes.best == 'application/json':
@@ -86,7 +82,6 @@ def fg_scan():
scan_groups = get_latest_scans(limit=25)
except Exception as e:
logger.error(f"Error fetching latest scans: {e}")
- flash(f"Error fetching scan data: {str(e)}", 'error')
scan_groups = []
return render_template('modules/quality/fg_scan.html', scan_groups=scan_groups)
diff --git a/app/modules/settings/routes.py b/app/modules/settings/routes.py
index 950dffc..e7aae50 100644
--- a/app/modules/settings/routes.py
+++ b/app/modules/settings/routes.py
@@ -185,7 +185,6 @@ def create_user():
password = request.form.get('password', '').strip()
confirm_password = request.form.get('confirm_password', '').strip()
is_active = request.form.get('is_active') == 'on'
- modules = request.form.getlist('modules')
# Validation
errors = []
@@ -202,8 +201,6 @@ def create_user():
errors.append("Passwords do not match")
if len(password) < 8:
errors.append("Password must be at least 8 characters")
- if not modules:
- errors.append("Select at least one module")
if errors:
conn = get_db()
@@ -251,13 +248,6 @@ def create_user():
(user_id, password_hash)
)
- # Grant module access
- for module in modules:
- cursor.execute(
- "INSERT IGNORE INTO user_modules (user_id, module_name) VALUES (%s, %s)",
- (user_id, module)
- )
-
conn.commit()
cursor.close()
@@ -311,11 +301,10 @@ def edit_user(user_id):
if request.method == 'GET':
cursor.close()
- available_modules = ['quality', 'settings']
return render_template('modules/settings/user_form.html',
user=user,
roles=roles,
- available_modules=available_modules,
+ available_modules=['quality', 'settings'],
user_modules=user_modules)
# Handle POST - Update user
@@ -326,7 +315,6 @@ def edit_user(user_id):
password = request.form.get('password', '').strip()
confirm_password = request.form.get('confirm_password', '').strip()
is_active = request.form.get('is_active') == 'on'
- modules = request.form.getlist('modules')
# Validation
errors = []
@@ -339,16 +327,13 @@ def edit_user(user_id):
errors.append("Passwords do not match")
if password and len(password) < 8:
errors.append("Password must be at least 8 characters")
- if not modules:
- errors.append("Select at least one module")
if errors:
cursor.close()
- available_modules = ['quality', 'settings']
return render_template('modules/settings/user_form.html',
user=user,
roles=roles,
- available_modules=available_modules,
+ available_modules=['quality', 'settings'],
user_modules=user_modules,
error="; ".join(errors))
@@ -366,14 +351,6 @@ def edit_user(user_id):
(password_hash, user_id)
)
- # Update module access
- cursor.execute("DELETE FROM user_modules WHERE user_id = %s", (user_id,))
- for module in modules:
- cursor.execute(
- "INSERT INTO user_modules (user_id, module_name) VALUES (%s, %s)",
- (user_id, module)
- )
-
conn.commit()
cursor.close()
@@ -381,11 +358,10 @@ def edit_user(user_id):
except Exception as e:
cursor.close()
- available_modules = ['quality', 'settings']
return render_template('modules/settings/user_form.html',
user=user,
roles=roles,
- available_modules=available_modules,
+ available_modules=['quality', 'settings'],
user_modules=user_modules,
error=f"Error updating user: {str(e)}")
@@ -862,6 +838,40 @@ def restore_database():
return jsonify({'error': str(e)}), 500
+@settings_bp.route('/api/database/tables', methods=['GET'])
+def get_database_tables():
+ """Get list of all database tables (dynamically fetched)"""
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ conn = get_db()
+ cursor = conn.cursor()
+
+ # Get list of all tables with their row counts
+ cursor.execute("""
+ SELECT TABLE_NAME, TABLE_ROWS
+ FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE()
+ ORDER BY TABLE_NAME
+ """)
+
+ tables = [{'name': row[0], 'rows': row[1] or 0} for row in cursor.fetchall()]
+ cursor.close()
+
+ return jsonify({
+ 'success': True,
+ 'tables': tables,
+ 'count': len(tables)
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': str(e)
+ }), 500
+
+
@settings_bp.route('/api/database/truncate', methods=['POST'])
def truncate_table():
"""Truncate (clear) a database table"""
diff --git a/app/modules/settings/warehouse_worker_management.py b/app/modules/settings/warehouse_worker_management.py
new file mode 100644
index 0000000..d4f1b48
--- /dev/null
+++ b/app/modules/settings/warehouse_worker_management.py
@@ -0,0 +1,329 @@
+"""
+Warehouse Worker Management Functions
+Handles worker-manager bindings and zone assignments for the warehouse module
+"""
+from app.database import get_db
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def assign_worker_to_manager(manager_id, worker_id, warehouse_zone=None):
+ """
+ Assign a warehouse worker to a manager with optional zone restriction
+
+ Args:
+ manager_id (int): Manager user ID
+ worker_id (int): Worker user ID
+ warehouse_zone (str, optional): Zone restriction (e.g., "Cold Storage", "High Shelf")
+
+ Returns:
+ tuple: (success: bool, message: str)
+ """
+ try:
+ if manager_id == worker_id:
+ return False, "Cannot assign a worker to themselves"
+
+ db = get_db()
+ cursor = db.cursor()
+
+ # Check if binding already exists
+ cursor.execute("""
+ SELECT id FROM worker_manager_bindings
+ WHERE manager_id = %s AND worker_id = %s
+ """, (manager_id, worker_id))
+
+ existing = cursor.fetchone()
+
+ if existing:
+ # Update existing binding
+ cursor.execute("""
+ UPDATE worker_manager_bindings
+ SET warehouse_zone = %s, is_active = 1, updated_at = NOW()
+ WHERE manager_id = %s AND worker_id = %s
+ """, (warehouse_zone, manager_id, worker_id))
+ message = f"Worker assignment updated (zone: {warehouse_zone or 'all zones'})"
+ else:
+ # Create new binding
+ cursor.execute("""
+ INSERT INTO worker_manager_bindings (manager_id, worker_id, warehouse_zone)
+ VALUES (%s, %s, %s)
+ """, (manager_id, worker_id, warehouse_zone))
+ message = f"Worker assigned to manager (zone: {warehouse_zone or 'all zones'})"
+
+ db.commit()
+ logger.info(f"✓ {message} - Manager ID: {manager_id}, Worker ID: {worker_id}")
+ return True, message
+
+ except Exception as e:
+ logger.error(f"Error assigning worker to manager: {e}")
+ return False, f"Error: {str(e)}"
+
+
+def unassign_worker_from_manager(manager_id, worker_id, soft_delete=True):
+ """
+ Remove a worker from a manager's supervision
+
+ Args:
+ manager_id (int): Manager user ID
+ worker_id (int): Worker user ID
+ soft_delete (bool): If True, set is_active=0; if False, delete record
+
+ Returns:
+ tuple: (success: bool, message: str)
+ """
+ try:
+ db = get_db()
+ cursor = db.cursor()
+
+ if soft_delete:
+ cursor.execute("""
+ UPDATE worker_manager_bindings
+ SET is_active = 0, updated_at = NOW()
+ WHERE manager_id = %s AND worker_id = %s
+ """, (manager_id, worker_id))
+ else:
+ cursor.execute("""
+ DELETE FROM worker_manager_bindings
+ WHERE manager_id = %s AND worker_id = %s
+ """, (manager_id, worker_id))
+
+ db.commit()
+ logger.info(f"✓ Worker unassigned - Manager ID: {manager_id}, Worker ID: {worker_id}")
+ return True, "Worker unassigned successfully"
+
+ except Exception as e:
+ logger.error(f"Error unassigning worker: {e}")
+ return False, f"Error: {str(e)}"
+
+
+def get_worker_binding_info(worker_id):
+ """
+ Get binding information for a worker (who manages them, what zone)
+
+ Args:
+ worker_id (int): Worker user ID
+
+ Returns:
+ dict: {manager_id, manager_username, manager_name, zone, is_active} or None
+ """
+ try:
+ db = get_db()
+ cursor = db.cursor()
+
+ cursor.execute("""
+ SELECT m.id, m.username, m.full_name, wmb.warehouse_zone, wmb.is_active
+ FROM worker_manager_bindings wmb
+ JOIN users m ON wmb.manager_id = m.id
+ WHERE wmb.worker_id = %s
+ LIMIT 1
+ """, (worker_id,))
+
+ result = cursor.fetchone()
+ if result:
+ return {
+ 'manager_id': result[0],
+ 'manager_username': result[1],
+ 'manager_name': result[2],
+ 'zone': result[3],
+ 'is_active': result[4]
+ }
+ return None
+
+ except Exception as e:
+ logger.error(f"Error getting worker binding info: {e}")
+ return None
+
+
+def get_available_workers_for_assignment(exclude_manager_id=None):
+ """
+ Get all warehouse_worker users available for assignment
+
+ Args:
+ exclude_manager_id (int, optional): Manager ID to exclude from results
+
+ Returns:
+ list: List of dicts {id, username, full_name, current_manager}
+ """
+ try:
+ db = get_db()
+ cursor = db.cursor()
+
+ query = """
+ SELECT u.id, u.username, u.full_name,
+ (SELECT m.full_name FROM worker_manager_bindings wmb
+ JOIN users m ON wmb.manager_id = m.id
+ WHERE wmb.worker_id = u.id AND wmb.is_active = 1 LIMIT 1) as current_manager
+ FROM users u
+ WHERE u.role = 'warehouse_worker'
+ ORDER BY u.full_name
+ """
+
+ cursor.execute(query)
+
+ results = []
+ for row in cursor.fetchall():
+ results.append({
+ 'id': row[0],
+ 'username': row[1],
+ 'full_name': row[2],
+ 'current_manager': row[3]
+ })
+
+ return results
+
+ except Exception as e:
+ logger.error(f"Error getting available workers: {e}")
+ return []
+
+
+def get_manager_assigned_workers(manager_id, include_inactive=False):
+ """
+ Get all workers assigned to a specific manager
+
+ Args:
+ manager_id (int): Manager user ID
+ include_inactive (bool): Whether to include inactive bindings
+
+ Returns:
+ list: List of dicts {id, username, full_name, zone, is_active}
+ """
+ try:
+ db = get_db()
+ cursor = db.cursor()
+
+ query = """
+ SELECT u.id, u.username, u.full_name, wmb.warehouse_zone, wmb.is_active
+ FROM worker_manager_bindings wmb
+ JOIN users u ON wmb.worker_id = u.id
+ WHERE wmb.manager_id = %s
+ """
+
+ params = [manager_id]
+
+ if not include_inactive:
+ query += " AND wmb.is_active = 1"
+
+ query += " ORDER BY u.full_name"
+
+ cursor.execute(query, params)
+
+ results = []
+ for row in cursor.fetchall():
+ results.append({
+ 'id': row[0],
+ 'username': row[1],
+ 'full_name': row[2],
+ 'zone': row[3],
+ 'is_active': row[4]
+ })
+
+ return results
+
+ except Exception as e:
+ logger.error(f"Error getting manager's workers: {e}")
+ return []
+
+
+def validate_zone_name(zone_name):
+ """
+ Validate zone name format (alphanumeric, spaces, hyphens, underscores allowed)
+
+ Args:
+ zone_name (str): Zone name to validate
+
+ Returns:
+ tuple: (is_valid: bool, message: str)
+ """
+ if not zone_name:
+ return True, "No zone restriction" # NULL zone is valid (all zones)
+
+ if not isinstance(zone_name, str):
+ return False, "Zone must be a string"
+
+ zone_name = zone_name.strip()
+
+ if len(zone_name) > 100:
+ return False, "Zone name too long (max 100 characters)"
+
+ if len(zone_name) < 2:
+ return False, "Zone name too short (min 2 characters)"
+
+ # Allow alphanumeric, spaces, hyphens, underscores
+ import re
+ if not re.match(r'^[a-zA-Z0-9\s\-_]+$', zone_name):
+ return False, "Zone name contains invalid characters"
+
+ return True, "Valid zone name"
+
+
+def get_warehouse_zones():
+ """
+ Get list of all warehouse zones in use
+
+ Returns:
+ list: List of zone names currently assigned to workers
+ """
+ try:
+ db = get_db()
+ cursor = db.cursor()
+
+ cursor.execute("""
+ SELECT DISTINCT warehouse_zone
+ FROM worker_manager_bindings
+ WHERE is_active = 1 AND warehouse_zone IS NOT NULL
+ ORDER BY warehouse_zone
+ """)
+
+ zones = [row[0] for row in cursor.fetchall()]
+ return zones
+
+ except Exception as e:
+ logger.error(f"Error getting warehouse zones: {e}")
+ return []
+
+
+def reassign_worker(worker_id, new_manager_id, warehouse_zone=None):
+ """
+ Reassign a worker from current manager to a new manager
+
+ Args:
+ worker_id (int): Worker user ID
+ new_manager_id (int): New manager user ID
+ warehouse_zone (str, optional): New zone restriction
+
+ Returns:
+ tuple: (success: bool, message: str)
+ """
+ try:
+ if worker_id == new_manager_id:
+ return False, "Cannot assign a worker to themselves"
+
+ db = get_db()
+ cursor = db.cursor()
+
+ # Deactivate old binding
+ cursor.execute("""
+ UPDATE worker_manager_bindings
+ SET is_active = 0
+ WHERE worker_id = %s AND is_active = 1
+ """, (worker_id,))
+
+ # Create new binding
+ cursor.execute("""
+ INSERT INTO worker_manager_bindings (manager_id, worker_id, warehouse_zone)
+ VALUES (%s, %s, %s)
+ ON DUPLICATE KEY UPDATE
+ is_active = 1,
+ warehouse_zone = %s,
+ updated_at = NOW()
+ """, (new_manager_id, worker_id, warehouse_zone, warehouse_zone))
+
+ db.commit()
+
+ logger.info(f"✓ Worker reassigned - Worker ID: {worker_id}, New Manager ID: {new_manager_id}")
+ return True, "Worker reassigned successfully"
+
+ except Exception as e:
+ logger.error(f"Error reassigning worker: {e}")
+ return False, f"Error: {str(e)}"
diff --git a/app/static/css/database_management.css b/app/static/css/database_management.css
new file mode 100644
index 0000000..d6d59d6
--- /dev/null
+++ b/app/static/css/database_management.css
@@ -0,0 +1,167 @@
+/* Database Management Module - Theme-Aware Styling */
+
+/* Modal Styling with Theme Support */
+#confirmTruncateModal .modal-content {
+ background-color: var(--bg-primary);
+ border-color: var(--border-color);
+ border-radius: 8px;
+ box-shadow: 0 10px 40px var(--card-shadow-hover);
+}
+
+#confirmTruncateModal .modal-header {
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ border-bottom-color: var(--border-color);
+ border-radius: 8px 8px 0 0;
+}
+
+#confirmTruncateModal .modal-header h5 {
+ color: white;
+ margin: 0;
+ font-weight: 600;
+}
+
+#confirmTruncateModal .modal-body {
+ color: var(--text-primary);
+ padding: 20px;
+}
+
+#confirmTruncateModal .modal-body p {
+ color: var(--text-primary);
+}
+
+#confirmTruncateModal .modal-body strong {
+ font-weight: 600;
+}
+
+/* Table name display box */
+#confirmTruncateModal .table-name-box {
+ background-color: var(--bg-secondary);
+ border: 2px solid #ef4444;
+ border-radius: 6px;
+ padding: 15px;
+ margin: 15px 0;
+}
+
+#confirmTruncateModal .table-name-box strong {
+ color: #ef4444;
+ font-size: 1.1em;
+ word-break: break-all;
+}
+
+/* Warning alert */
+#confirmTruncateModal .warning-alert {
+ background-color: rgba(239, 68, 68, 0.1);
+ border: 1px solid #ef4444;
+ border-radius: 6px;
+ padding: 12px 15px;
+ margin: 15px 0;
+ color: var(--text-primary);
+}
+
+#confirmTruncateModal .warning-alert i {
+ color: #ef4444;
+ margin-right: 8px;
+}
+
+/* Input field styling */
+#confirmTruncateModal #confirm-table-input {
+ background-color: var(--input-bg);
+ border: 1px solid var(--input-border);
+ color: var(--text-primary);
+ border-radius: 6px;
+ padding: 10px 12px;
+ font-size: 14px;
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
+}
+
+#confirmTruncateModal #confirm-table-input:focus {
+ background-color: var(--input-bg);
+ border-color: #ef4444;
+ color: var(--text-primary);
+ box-shadow: 0 0 0 0.2rem rgba(239, 68, 68, 0.15);
+ outline: none;
+}
+
+#confirmTruncateModal #confirm-table-input::placeholder {
+ color: var(--text-secondary);
+ opacity: 0.7;
+}
+
+/* Modal footer */
+#confirmTruncateModal .modal-footer {
+ background-color: var(--bg-secondary);
+ border-top: 1px solid var(--border-color);
+ border-radius: 0 0 8px 8px;
+ padding: 15px 20px;
+}
+
+/* Cancel button */
+#confirmTruncateModal .btn-secondary {
+ background-color: var(--bg-tertiary);
+ color: var(--text-primary);
+ border-color: var(--border-color);
+ transition: all 0.3s ease;
+}
+
+#confirmTruncateModal .btn-secondary:hover:not(:disabled) {
+ background-color: var(--border-color);
+ color: var(--text-primary);
+}
+
+/* Delete button */
+#confirmTruncateModal .btn-danger {
+ background-color: #ef4444;
+ border-color: #dc2626;
+ color: white;
+ font-weight: 600;
+ transition: all 0.3s ease;
+}
+
+#confirmTruncateModal .btn-danger:hover:not(:disabled) {
+ background-color: #dc2626;
+ border-color: #b91c1c;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
+}
+
+#confirmTruncateModal .btn-danger:disabled {
+ background-color: #9ca3af;
+ border-color: #6b7280;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+/* Modal backdrop */
+.modal-backdrop.show {
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+/* Help text styling */
+#confirmTruncateModal .text-muted {
+ color: var(--text-secondary) !important;
+ font-size: 0.85em;
+}
+
+/* Label styling */
+#confirmTruncateModal .modal-body > p:first-of-type {
+ font-size: 0.95em;
+}
+
+/* Dark mode specific adjustments */
+[data-theme="dark"] #confirmTruncateModal .modal-body strong {
+ color: #ef4444;
+}
+
+[data-theme="dark"] #confirmTruncateModal .table-name-box strong {
+ color: #ff6b6b;
+}
+
+[data-theme="dark"] #confirmTruncateModal .warning-alert {
+ background-color: rgba(239, 68, 68, 0.2);
+}
+
+[data-theme="dark"] #confirmTruncateModal #confirm-table-input:focus {
+ background-color: var(--input-bg);
+ border-color: #ff6b6b;
+ box-shadow: 0 0 0 0.2rem rgba(255, 107, 107, 0.15);
+}
diff --git a/app/templates/modules/quality/fg_scan.html b/app/templates/modules/quality/fg_scan.html
index 15bcd9f..ef3f286 100644
--- a/app/templates/modules/quality/fg_scan.html
+++ b/app/templates/modules/quality/fg_scan.html
@@ -921,12 +921,58 @@ function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
- notification.style.opacity = '1';
+ // Apply inline styles to ensure proper positioning and appearance
+ notification.style.cssText = `
+ position: fixed;
+ top: 30px;
+ right: 30px;
+ padding: 12px 20px;
+ border-radius: 6px;
+ z-index: 9999;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.25);
+ font-weight: 600;
+ font-size: 14px;
+ white-space: nowrap;
+ opacity: 1;
+ animation: slideInRight 0.3s ease-out, slideOutRight 0.3s ease-in 2.7s forwards;
+ `;
document.body.appendChild(notification);
+ // Ensure animations exist
+ if (!document.getElementById('notification-styles')) {
+ const style = document.createElement('style');
+ style.id = 'notification-styles';
+ style.textContent = `
+ @keyframes slideInRight {
+ from {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+
+ @keyframes slideOutRight {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // Remove notification after animation completes
setTimeout(() => {
- notification.style.opacity = '0';
- setTimeout(() => notification.remove(), 300);
+ if (notification.parentNode) {
+ notification.parentNode.removeChild(notification);
+ }
}, 3000);
}
diff --git a/app/templates/modules/settings/database_management.html b/app/templates/modules/settings/database_management.html
index 93c5db0..c6844ce 100644
--- a/app/templates/modules/settings/database_management.html
+++ b/app/templates/modules/settings/database_management.html
@@ -2,6 +2,10 @@
{% block title %}Database Management{% endblock %}
+{% block extra_css %}
+
+{% endblock %}
+
{% block content %}
@@ -328,15 +332,12 @@
-
@@ -411,21 +412,26 @@
-
-
-
Confirm Table Clear
+
+
+
Confirm Table Clear
-
-
You are about to permanently delete all data from:
-
-
This action cannot be undone!
-
Please ensure you have a backup before proceeding.
+
+
You are about to permanently delete all data from:
+
+
+ This action CANNOT be undone!
+
+
+
To confirm, please type the table name:
+
+ This is a safety measure to prevent accidental data deletion.
-
@@ -434,12 +440,27 @@
{% endblock %}
diff --git a/app/templates/modules/settings/user_form.html b/app/templates/modules/settings/user_form.html
index 74ae150..6bfcd34 100644
--- a/app/templates/modules/settings/user_form.html
+++ b/app/templates/modules/settings/user_form.html
@@ -77,14 +77,100 @@
- User's access level
+
+ User's access level and role. See role descriptions below.
+
+
+
+
+
+
+
+
+
+
+
+
Role
+
Level
+
Modules
+
Permissions & Description
+
+
+
+
+
Super Admin
+
100
+
All
+
Unrestricted access to entire system. Can manage all users and configuration.
+
+
+
Admin
+
90
+
All
+
Full system administration. Can manage users, settings, database, backups.
+
+
+
Manager - Quality
+
70
+
Quality
+
Create, edit, delete quality inspections. Can export and download quality reports.
+
+
+
Manager - Warehouse
+
75
+
Warehouse
+
Full warehouse input and analytics access. Can manage assigned warehouse workers and zones.
+
+
+
Worker - Quality
+
50
+
Quality
+
Create and view quality inspections only. Cannot edit, delete, or view reports.
+
+
+
Worker - Warehouse
+
35
+
Warehouse
+
Input pages only (set locations, create entries). No access to reports or analytics. Must be assigned to a Manager.
+
+
+
+
+
+ Important: Warehouse workers are assigned to a manager for supervision.
+ Quality and Warehouse modules are separate - users can have one or both module access.
+
@@ -94,7 +180,7 @@
Password
{% if not user %}*{% endif %}
-
{% if user %}Leave blank to keep current password{% else %}Minimum 8 characters{% endif %}
@@ -105,34 +191,12 @@
Confirm Password
{% if not user %}*{% endif %}
-
Re-enter password to confirm