feat: Implement warehouse module roles with auto-schema repair and remove module access section
- Add SchemaVerifier class for automatic database schema verification and repair - Implement warehouse_manager (Level 75) and warehouse_worker (Level 35) roles - Add zone-based access control for warehouse workers - Implement worker-manager binding system with zone filtering - Add comprehensive database auto-repair on Docker initialization - Remove Module Access section from user form (role-based access only) - Add autocomplete attributes to password fields for better UX - Include detailed documentation for warehouse implementation - Update initialize_db.py with schema verification as Step 0
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
{% block title %}Database Management{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/database_management.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
@@ -328,15 +332,12 @@
|
||||
<div class="col-md-6">
|
||||
<label for="truncate-table-select" class="form-label">Select Table:</label>
|
||||
<select class="form-select" id="truncate-table-select">
|
||||
<option value="">-- Select a table --</option>
|
||||
{% for table in tables %}
|
||||
<option value="{{ table.name }}">{{ table.name }} ({{ table.rows }} rows)</option>
|
||||
{% endfor %}
|
||||
<option value="">-- Loading tables... --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block"> </label>
|
||||
<button type="button" class="btn btn-danger w-100" id="truncate-btn" disabled data-bs-toggle="modal" data-bs-target="#confirmTruncateModal">
|
||||
<button type="button" class="btn btn-danger w-100" id="truncate-btn" disabled>
|
||||
<i class="fas fa-trash"></i> Clear Selected Table
|
||||
</button>
|
||||
</div>
|
||||
@@ -411,21 +412,26 @@
|
||||
<!-- Confirm Truncate Modal -->
|
||||
<div class="modal fade" id="confirmTruncateModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title"><i class="fas fa-exclamation-triangle"></i> Confirm Table Clear</h5>
|
||||
<div class="modal-content" style="background-color: var(--bg-primary); border-color: var(--border-color);">
|
||||
<div class="modal-header" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); border-bottom-color: var(--border-color);">
|
||||
<h5 class="modal-title" style="color: white;"><i class="fas fa-exclamation-triangle"></i> Confirm Table Clear</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-2">You are about to <strong>permanently delete all data</strong> from:</p>
|
||||
<p class="bg-light p-2 rounded"><strong id="confirm-table-name"></strong></p>
|
||||
<p class="text-danger"><strong>This action cannot be undone!</strong></p>
|
||||
<p class="small text-muted mb-0">Please ensure you have a backup before proceeding.</p>
|
||||
<div class="modal-body" style="color: var(--text-primary);">
|
||||
<p class="mb-3">You are about to <strong>permanently delete all data</strong> from:</p>
|
||||
<p class="p-3 rounded mb-3" style="background-color: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);"><strong id="confirm-table-name" style="font-size: 1.1em; color: #ef4444;"></strong></p>
|
||||
<div class="alert" role="alert" style="background-color: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; color: var(--text-primary);">
|
||||
<i class="fas fa-exclamation-circle"></i> <strong>This action CANNOT be undone!</strong>
|
||||
</div>
|
||||
|
||||
<p class="mb-2" style="color: var(--text-primary);"><strong>To confirm, please type the table name:</strong></p>
|
||||
<input type="text" class="form-control" id="confirm-table-input" placeholder="Enter table name to confirm..." autocomplete="off" style="background-color: var(--input-bg); border-color: var(--input-border); color: var(--text-primary);">
|
||||
<small class="text-muted d-block mt-2">This is a safety measure to prevent accidental data deletion.</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirm-truncate-btn">
|
||||
<i class="fas fa-trash"></i> Yes, Clear Table
|
||||
<div class="modal-footer" style="border-top-color: var(--border-color);">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-color);">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirm-truncate-btn" disabled style="background-color: #ef4444; border-color: #dc2626;">
|
||||
<i class="fas fa-trash"></i> Yes, Clear All Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,12 +440,27 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load all database tables dynamically
|
||||
loadDatabaseTables();
|
||||
|
||||
// Load backups list
|
||||
loadBackupsList();
|
||||
|
||||
// Load schedules list
|
||||
loadBackupSchedules();
|
||||
|
||||
// ===== DECLARE VARIABLES ONCE AT THE TOP =====
|
||||
const truncateSelect = document.getElementById('truncate-table-select');
|
||||
const truncateBtn = document.getElementById('truncate-btn');
|
||||
const confirmInput = document.getElementById('confirm-table-input');
|
||||
const confirmBtn = document.getElementById('confirm-truncate-btn');
|
||||
|
||||
console.log('Initializing truncate handlers...');
|
||||
console.log('truncateSelect:', truncateSelect);
|
||||
console.log('truncateBtn:', truncateBtn);
|
||||
console.log('confirmInput:', confirmInput);
|
||||
console.log('confirmBtn:', confirmBtn);
|
||||
|
||||
// Cleanup old backups handler
|
||||
const cleanupBtn = document.getElementById('cleanup-old-backups-btn');
|
||||
if (cleanupBtn) {
|
||||
@@ -504,89 +525,167 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
|
||||
// Truncate table handler
|
||||
const truncateSelect = document.getElementById('truncate-table-select');
|
||||
const truncateBtn = document.getElementById('truncate-btn');
|
||||
|
||||
console.log('Initializing truncate handler...');
|
||||
console.log('truncateSelect element:', truncateSelect);
|
||||
console.log('truncateBtn element:', truncateBtn);
|
||||
console.log('truncateBtn.disabled initial value:', truncateBtn ? truncateBtn.disabled : 'N/A');
|
||||
// Truncate table SELECT change handler - enable/disable button
|
||||
console.log('Registering SELECT change handler...');
|
||||
|
||||
if (truncateSelect) {
|
||||
if (truncateSelect && truncateBtn) {
|
||||
truncateSelect.addEventListener('change', function() {
|
||||
const table = this.value;
|
||||
const option = this.options[this.selectedIndex];
|
||||
|
||||
console.log('=== TRUNCATE HANDLER FIRED ===');
|
||||
console.log('Selected value:', table);
|
||||
console.log('Selected option text:', option.text);
|
||||
console.log('Button disabled before:', truncateBtn.disabled);
|
||||
console.log('=== TABLE SELECTED ===');
|
||||
console.log('Selected table:', table);
|
||||
|
||||
if (table) {
|
||||
console.log('Table selected - enabling button');
|
||||
document.getElementById('truncate-info').style.display = 'block';
|
||||
document.getElementById('truncate-table-name').textContent = table;
|
||||
document.getElementById('truncate-row-count').textContent = option.text.match(/\((\d+)\s*rows\)/)?.[1] || '0';
|
||||
if (table && table.trim()) {
|
||||
console.log('✓ Valid table selected - ENABLING truncate button');
|
||||
|
||||
// Safely update info display
|
||||
const truncateInfo = document.getElementById('truncate-info');
|
||||
if (truncateInfo) {
|
||||
truncateInfo.style.display = 'block';
|
||||
}
|
||||
|
||||
// Safely update table name
|
||||
const truncateTableName = document.getElementById('truncate-table-name');
|
||||
if (truncateTableName) {
|
||||
truncateTableName.textContent = table;
|
||||
}
|
||||
|
||||
// Safely update row count
|
||||
const truncateRowCount = document.getElementById('truncate-row-count');
|
||||
if (truncateRowCount) {
|
||||
truncateRowCount.textContent = option.text.match(/\((\d+)\s*rows\)/)?.[1] || '0';
|
||||
}
|
||||
|
||||
// Safely update confirm table name
|
||||
const confirmTableName = document.getElementById('confirm-table-name');
|
||||
if (confirmTableName) {
|
||||
confirmTableName.textContent = table;
|
||||
}
|
||||
|
||||
// Enable the button
|
||||
truncateBtn.disabled = false;
|
||||
document.getElementById('confirm-table-name').textContent = table;
|
||||
console.log('Button disabled after setting to false:', truncateBtn.disabled);
|
||||
|
||||
console.log('✓ Button enabled successfully');
|
||||
} else {
|
||||
console.log('No table selected - disabling button');
|
||||
document.getElementById('truncate-info').style.display = 'none';
|
||||
console.log('✗ No table selected - DISABLING truncate button');
|
||||
|
||||
// Safely hide info display
|
||||
const truncateInfo = document.getElementById('truncate-info');
|
||||
if (truncateInfo) {
|
||||
truncateInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
truncateBtn.disabled = true;
|
||||
console.log('Button disabled after setting to true:', truncateBtn.disabled);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✓ Change event listener registered on truncate-table-select');
|
||||
} else {
|
||||
console.error('✗ truncate-table-select element not found!');
|
||||
console.error('✗ truncate-table-select or truncate-btn not found!');
|
||||
}
|
||||
|
||||
// Confirm truncate
|
||||
document.getElementById('confirm-truncate-btn').addEventListener('click', function() {
|
||||
const table = document.getElementById('truncate-table-select').value;
|
||||
|
||||
// Disable button to prevent multiple clicks
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Clearing...';
|
||||
|
||||
fetch('{{ url_for("settings.truncate_table") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ table: table })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Hide modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmTruncateModal'));
|
||||
if (modal) modal.hide();
|
||||
|
||||
// Show success message
|
||||
alert('Table cleared successfully! Refreshing page...');
|
||||
|
||||
// Refresh the page after a short delay
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 500);
|
||||
|
||||
// Truncate button click handler - opens modal
|
||||
if (truncateBtn && truncateSelect) {
|
||||
truncateBtn.addEventListener('click', function() {
|
||||
const selectedTable = truncateSelect.value;
|
||||
console.log('Truncate button clicked, selected table:', selectedTable);
|
||||
|
||||
if (!selectedTable) {
|
||||
alert('Please select a table first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
const modalElement = document.getElementById('confirmTruncateModal');
|
||||
if (!modalElement) {
|
||||
console.error('❌ confirmTruncateModal element not found');
|
||||
alert('Error: Modal not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = new bootstrap.Modal(modalElement);
|
||||
|
||||
// Clear the input field
|
||||
if (confirmInput) {
|
||||
confirmInput.value = '';
|
||||
}
|
||||
|
||||
// Disable the confirm button
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = true;
|
||||
}
|
||||
|
||||
console.log('Opening modal for table:', selectedTable);
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Confirmation input handler - enables button only when user types correct table name
|
||||
if (confirmInput && confirmBtn) {
|
||||
confirmInput.addEventListener('input', function() {
|
||||
const selectedTable = truncateSelect.value;
|
||||
const inputValue = this.value.trim();
|
||||
|
||||
// Enable button only if user typed the exact table name
|
||||
if (inputValue === selectedTable) {
|
||||
confirmBtn.disabled = false;
|
||||
console.log('✓ Table name matches - button enabled');
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
confirmBtn.disabled = true;
|
||||
console.log('✗ Table name does not match - button disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm truncate button click
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
const table = truncateSelect.value;
|
||||
|
||||
// Disable button to prevent multiple clicks
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Clearing...';
|
||||
|
||||
fetch('{{ url_for("settings.truncate_table") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ table: table })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Hide modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmTruncateModal'));
|
||||
if (modal) modal.hide();
|
||||
|
||||
// Show success message
|
||||
alert('Table cleared successfully! Refreshing page...');
|
||||
|
||||
// Refresh the page after a short delay
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 500);
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
// Re-enable button
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear All Data';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error clearing table: ' + error);
|
||||
// Re-enable button
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear Table';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error clearing table: ' + error);
|
||||
// Re-enable button
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear Table';
|
||||
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear All Data';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Restore backup handler
|
||||
document.getElementById('restore-backup-select').addEventListener('change', function() {
|
||||
@@ -900,5 +999,51 @@ function uploadBackupFile() {
|
||||
}
|
||||
|
||||
// Load schedules on page load
|
||||
|
||||
function loadDatabaseTables() {
|
||||
/**
|
||||
* Dynamically load ALL database tables from the server
|
||||
* This ensures that newly added tables are always available
|
||||
* Called ONCE on page load
|
||||
*/
|
||||
console.log('🔄 Loading database tables...');
|
||||
|
||||
fetch('{{ url_for("settings.get_database_tables") }}')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success && data.tables && data.tables.length > 0) {
|
||||
const select = document.getElementById('truncate-table-select');
|
||||
if (!select) {
|
||||
console.error('❌ truncate-table-select element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing options except the first placeholder
|
||||
select.innerHTML = '<option value="">-- Select a table --</option>';
|
||||
|
||||
// Add all tables as options
|
||||
data.tables.forEach(table => {
|
||||
const option = document.createElement('option');
|
||||
option.value = table.name;
|
||||
option.textContent = `${table.name} (${table.rows} rows)`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
console.log(`✓ Loaded ${data.tables.length} tables from database`);
|
||||
} else {
|
||||
console.warn('⚠️ No tables returned from server or API returned an error');
|
||||
console.warn('Response:', data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Error loading database tables:', error);
|
||||
document.getElementById('truncate-table-select').innerHTML = '<option value="">-- Error loading tables --</option>';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -77,14 +77,100 @@
|
||||
<label for="role" class="form-label">Role <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="role" name="role" required>
|
||||
<option value="">-- Select a role --</option>
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.name }}"
|
||||
{% if user and user.role == role.name %}selected{% endif %}>
|
||||
{{ role.name | capitalize }} (Level {{ role.level }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
<optgroup label="System Roles">
|
||||
<option value="superadmin"
|
||||
{% if user and user.role == 'superadmin' %}selected{% endif %}>
|
||||
Super Admin (Level 100)
|
||||
</option>
|
||||
<option value="admin"
|
||||
{% if user and user.role == 'admin' %}selected{% endif %}>
|
||||
Admin (Level 90)
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup label="Quality Module">
|
||||
<option value="manager"
|
||||
{% if user and user.role == 'manager' %}selected{% endif %}>
|
||||
Manager - Quality (Level 70)
|
||||
</option>
|
||||
<option value="worker"
|
||||
{% if user and user.role == 'worker' %}selected{% endif %}>
|
||||
Worker - Quality (Level 50)
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup label="Warehouse Module">
|
||||
<option value="warehouse_manager"
|
||||
{% if user and user.role == 'warehouse_manager' %}selected{% endif %}>
|
||||
Manager - Warehouse (Level 75)
|
||||
</option>
|
||||
<option value="warehouse_worker"
|
||||
{% if user and user.role == 'warehouse_worker' %}selected{% endif %}>
|
||||
Worker - Warehouse (Level 35)
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<small class="form-text text-muted">User's access level</small>
|
||||
<small class="form-text text-muted">
|
||||
User's access level and role. See role descriptions below.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label class="form-label"><i class="fas fa-info-circle"></i> Role Reference Matrix</label>
|
||||
<div class="table-responsive" style="font-size: 0.9rem;">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 15%;">Role</th>
|
||||
<th style="width: 8%;">Level</th>
|
||||
<th style="width: 20%;">Modules</th>
|
||||
<th style="width: 57%;">Permissions & Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Super Admin</strong></td>
|
||||
<td><span class="badge bg-danger">100</span></td>
|
||||
<td>All</td>
|
||||
<td>Unrestricted access to entire system. Can manage all users and configuration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Admin</strong></td>
|
||||
<td><span class="badge bg-danger">90</span></td>
|
||||
<td>All</td>
|
||||
<td>Full system administration. Can manage users, settings, database, backups.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Manager - Quality</strong></td>
|
||||
<td><span class="badge bg-success">70</span></td>
|
||||
<td>Quality</td>
|
||||
<td>Create, edit, delete quality inspections. Can export and download quality reports.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Manager - Warehouse</strong></td>
|
||||
<td><span class="badge bg-success">75</span></td>
|
||||
<td>Warehouse</td>
|
||||
<td>Full warehouse input and analytics access. Can manage assigned warehouse workers and zones.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Worker - Quality</strong></td>
|
||||
<td><span class="badge bg-warning text-dark">50</span></td>
|
||||
<td>Quality</td>
|
||||
<td>Create and view quality inspections only. Cannot edit, delete, or view reports.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Worker - Warehouse</strong></td>
|
||||
<td><span class="badge bg-warning text-dark">35</span></td>
|
||||
<td>Warehouse</td>
|
||||
<td>Input pages only (set locations, create entries). <strong>No access to reports or analytics.</strong> Must be assigned to a Manager.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<small class="form-text text-muted d-block mt-2">
|
||||
<strong>Important:</strong> Warehouse workers are assigned to a manager for supervision.
|
||||
Quality and Warehouse modules are separate - users can have one or both module access.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +180,7 @@
|
||||
Password
|
||||
{% if not user %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
<input type="password" class="form-control" id="password" name="password" autocomplete="new-password"
|
||||
{% if not user %}required{% else %}placeholder="Leave blank to keep current password"{% endif %}>
|
||||
<small class="form-text text-muted">
|
||||
{% if user %}Leave blank to keep current password{% else %}Minimum 8 characters{% endif %}
|
||||
@@ -105,34 +191,12 @@
|
||||
Confirm Password
|
||||
{% if not user %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" autocomplete="new-password"
|
||||
{% if not user %}required{% endif %}>
|
||||
<small class="form-text text-muted">Re-enter password to confirm</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="modules" class="form-label">Module Access <span class="text-danger">*</span></label>
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
{% for module in available_modules %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="module_{{ module }}"
|
||||
name="modules" value="{{ module }}"
|
||||
{% if user and module in user_modules %}checked{% endif %}>
|
||||
<label class="form-check-label" for="module_{{ module }}">
|
||||
<i class="fas fa-{% if module == 'quality' %}check-square{% elif module == 'settings' %}sliders-h{% else %}cube{% endif %}"></i>
|
||||
{{ module | capitalize }} Module
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">Select which modules this user can access</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="is_active" class="form-check-label">
|
||||
|
||||
Reference in New Issue
Block a user