Files
quality_recticel/py_app/app/templates/role_permissions.html

748 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Role Permissions Management{% endblock %}
{% block head %}
<style>
.permissions-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.header-section {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.header-section h2 {
margin: 0 0 10px 0;
font-size: 28px;
font-weight: 600;
}
.header-section p {
margin: 0;
opacity: 0.9;
font-size: 16px;
}
.role-tabs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
margin-bottom: 25px;
background-color: #f8f9fa;
padding: 8px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.role-tab {
padding: 15px 20px;
cursor: pointer;
background-color: white;
border: 2px solid transparent;
border-radius: 8px;
text-align: center;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.role-tab.active {
background: linear-gradient(135deg, #007bff, #0056b3);
color: white;
border-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
}
.role-tab:hover:not(.active) {
background-color: #e3f2fd;
border-color: #007bff;
transform: translateY(-1px);
}
.permission-tree {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.page-section {
background: white;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.page-section:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(0,0,0,0.15);
}
.page-header {
padding: 18px 24px;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
color: white;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.page-header:hover::before {
left: 100%;
}
.page-header.expanded {
background: linear-gradient(135deg, #007bff, #0056b3);
}
.expand-icon {
transition: transform 0.3s;
font-size: 12px;
}
.expanded .expand-icon {
transform: rotate(90deg);
}
.page-content {
display: none;
padding: 0;
}
.page-content.expanded {
display: block;
}
.section-item {
border-bottom: 1px solid #f1f1f1;
}
.section-item:last-child {
border-bottom: none;
}
.section-header {
padding: 16px 24px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
color: #495057;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.section-header:hover {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border-left-color: #2196f3;
color: #1565c0;
}
.section-header.expanded {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
color: #1976d2;
border-left-color: #1976d2;
}
.section-content {
display: none;
padding: 20px 24px;
background: linear-gradient(145deg, #ffffff, #f8f9fa);
}
.section-content.expanded {
display: block;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
margin-top: 10px;
}
.action-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: white;
border-radius: 8px;
border: 2px solid #f1f3f4;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.action-item:hover {
border-color: #007bff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,123,255,0.15);
}
.action-item.granted {
border-color: #28a745;
background: linear-gradient(145deg, #ffffff, #f8fff9);
}
.action-label {
display: flex;
align-items: center;
font-size: 14px;
color: #495057;
font-weight: 500;
}
.action-icon {
width: 20px;
height: 20px;
margin-right: 12px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: white;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.action-view { background-color: #28a745; }
.action-create { background-color: #007bff; }
.action-edit { background-color: #ffc107; color: #212529; }
.action-delete { background-color: #dc3545; }
.action-upload { background-color: #6f42c1; }
.action-download { background-color: #20c997; }
.action-export { background-color: #fd7e14; }
.action-import { background-color: #e83e8c; }
.permission-toggle {
position: relative;
display: inline-block;
width: 50px;
height: 25px;
}
.permission-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 25px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 19px;
width: 19px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #007bff;
}
input:checked + .toggle-slider:before {
transform: translateX(25px);
}
.permission-summary {
background: linear-gradient(145deg, #ffffff, #f8f9fa);
padding: 25px;
margin-bottom: 25px;
border-radius: 12px;
border-left: 5px solid #007bff;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.summary-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 20px;
margin-top: 15px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: transform 0.2s ease;
}
.stat-item:hover {
transform: translateY(-2px);
}
.stat-number {
font-size: 28px;
font-weight: bold;
color: #007bff;
margin-bottom: 5px;
}
.stat-label {
font-size: 11px;
color: #6c757d;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
}
.action-buttons {
margin-top: 30px;
padding: 25px;
background: linear-gradient(145deg, #ffffff, #f8f9fa);
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn-primary {
background: linear-gradient(135deg, #007bff, #0056b3);
color: white;
}
.btn-primary:hover {
background: linear-gradient(135deg, #0056b3, #004494);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d, #545b62);
color: white;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #545b62, #383d41);
}
.btn-success {
background: linear-gradient(135deg, #28a745, #1e7e34);
color: white;
}
.btn-success:hover {
background: linear-gradient(135deg, #1e7e34, #155724);
}
/* Responsive Design */
@media (max-width: 768px) {
.permissions-container {
padding: 10px;
}
.role-tabs {
grid-template-columns: 1fr;
}
.permission-tree {
grid-template-columns: 1fr;
}
.action-grid {
grid-template-columns: 1fr;
}
.summary-stats {
grid-template-columns: repeat(2, 1fr);
}
.action-buttons {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
{% endblock %}
{% block content %}
<div class="permissions-container">
<div class="header-section">
<h2>Role Permissions Management</h2>
<p>Configure granular access permissions for each role in the system</p>
</div>
<!-- Role Tabs -->
<div class="role-tabs">
{% for role_name, role_data in roles.items() %}
<div class="role-tab {% if loop.first %}active{% endif %}"
data-role="{{ role_name }}"
onclick="switchRole('{{ role_name }}')">
<div>{{ role_data.display_name }}</div>
<small>Level {{ role_data.level }}</small>
</div>
{% endfor %}
</div>
{% for role_name, role_data in roles.items() %}
<div class="role-content" id="role-{{ role_name }}"
style="{% if not loop.first %}display: none;{% endif %}">
<!-- Permission Summary -->
<div class="permission-summary">
<h4>{{ role_data.display_name }} Permissions Summary</h4>
<p>{{ role_data.description }}</p>
<div class="summary-stats">
<div class="stat-item">
<div class="stat-number" id="total-permissions-{{ role_name }}">0</div>
<div class="stat-label">Total Permissions</div>
</div>
<div class="stat-item">
<div class="stat-number" id="granted-permissions-{{ role_name }}">0</div>
<div class="stat-label">Granted</div>
</div>
<div class="stat-item">
<div class="stat-number" id="denied-permissions-{{ role_name }}">0</div>
<div class="stat-label">Denied</div>
</div>
</div>
</div>
<!-- Permission Tree -->
<div class="permission-tree">
{% for page_key, page_data in pages.items() %}
<div class="page-section">
<div class="page-header" onclick="togglePage('{{ role_name }}', '{{ page_key }}')">
<span>
<span class="expand-icon"></span>
{{ page_data.name }}
</span>
<span class="page-stats" id="page-stats-{{ role_name }}-{{ page_key }}">0/0</span>
</div>
<div class="page-content" id="page-content-{{ role_name }}-{{ page_key }}">
{% for section_key, section_data in page_data.sections.items() %}
<div class="section-item">
<div class="section-header" onclick="toggleSection('{{ role_name }}', '{{ page_key }}', '{{ section_key }}')">
<span>
<span class="expand-icon"></span>
{{ section_data.name }}
</span>
<span class="section-stats" id="section-stats-{{ role_name }}-{{ page_key }}-{{ section_key }}">0/{{ section_data.actions|length }}</span>
</div>
<div class="section-content" id="section-content-{{ role_name }}-{{ page_key }}-{{ section_key }}">
<div class="action-grid">
{% for action in section_data.actions %}
<div class="action-item" id="action-{{ role_name }}-{{ page_key }}-{{ section_key }}-{{ action }}">
<div class="action-label">
<div class="action-icon action-{{ action }}">
{% if action == 'view' %}👁{% elif action == 'create' %}{% elif action == 'edit' %}✏️{% elif action == 'delete' %}🗑{% elif action == 'upload' %}📤{% elif action == 'download' %}📥{% elif action == 'export' %}📊{% elif action == 'import' %}📈{% endif %}
</div>
<span>{{ action_names.get(action, action) }}</span>
</div>
<label class="permission-toggle">
<input type="checkbox"
data-role="{{ role_name }}"
data-permission="{{ page_key }}.{{ section_key }}.{{ action }}"
onchange="updatePermission('{{ role_name }}', '{{ page_key }}.{{ section_key }}.{{ action }}', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-secondary" onclick="resetToDefaults('{{ role_name }}')">Reset to Defaults</button>
<button class="btn btn-primary" onclick="savePermissions('{{ role_name }}')">Save Changes</button>
<button class="btn btn-success" onclick="copyFromRole('{{ role_name }}')">Copy from Another Role</button>
</div>
</div>
{% endfor %}
</div>
<script>
let currentRole = '{{ roles.keys()|first }}';
let permissions = {{ permissions_json|safe }};
let rolePermissions = {{ role_permissions_json|safe }};
function switchRole(roleName) {
// Hide all role contents
document.querySelectorAll('.role-content').forEach(content => {
content.style.display = 'none';
});
// Remove active class from all tabs
document.querySelectorAll('.role-tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected role content
document.getElementById('role-' + roleName).style.display = 'block';
// Add active class to selected tab
document.querySelector(`[data-role="${roleName}"]`).classList.add('active');
currentRole = roleName;
loadPermissions(roleName);
}
function togglePage(roleName, pageKey) {
const header = document.querySelector(`#page-content-${roleName}-${pageKey}`).previousElementSibling;
const content = document.getElementById(`page-content-${roleName}-${pageKey}`);
if (content.classList.contains('expanded')) {
content.classList.remove('expanded');
header.classList.remove('expanded');
} else {
content.classList.add('expanded');
header.classList.add('expanded');
}
}
function toggleSection(roleName, pageKey, sectionKey) {
const header = document.querySelector(`#section-content-${roleName}-${pageKey}-${sectionKey}`).previousElementSibling;
const content = document.getElementById(`section-content-${roleName}-${pageKey}-${sectionKey}`);
if (content.classList.contains('expanded')) {
content.classList.remove('expanded');
header.classList.remove('expanded');
} else {
content.classList.add('expanded');
header.classList.add('expanded');
}
}
function loadPermissions(roleName) {
const rolePerms = rolePermissions[roleName] || [];
// Reset all checkboxes and update visual feedback
document.querySelectorAll(`input[data-role="${roleName}"]`).forEach(checkbox => {
const permissionKey = checkbox.getAttribute('data-permission');
const isGranted = rolePerms.includes(permissionKey);
checkbox.checked = isGranted;
// Update visual feedback
const parts = permissionKey.split('.');
const actionElement = document.getElementById(`action-${roleName}-${parts[0]}-${parts[1]}-${parts[2]}`);
if (actionElement) {
if (isGranted) {
actionElement.classList.add('granted');
} else {
actionElement.classList.remove('granted');
}
}
});
updateAllStats(roleName);
}
function updatePermission(roleName, permissionKey, granted) {
// Update local state
if (!rolePermissions[roleName]) {
rolePermissions[roleName] = [];
}
if (granted && !rolePermissions[roleName].includes(permissionKey)) {
rolePermissions[roleName].push(permissionKey);
} else if (!granted) {
const index = rolePermissions[roleName].indexOf(permissionKey);
if (index > -1) {
rolePermissions[roleName].splice(index, 1);
}
}
// Update visual feedback
const parts = permissionKey.split('.');
const actionElement = document.getElementById(`action-${roleName}-${parts[0]}-${parts[1]}-${parts[2]}`);
if (actionElement) {
if (granted) {
actionElement.classList.add('granted');
} else {
actionElement.classList.remove('granted');
}
}
updateAllStats(roleName);
}
function updateAllStats(roleName) {
const totalPerms = document.querySelectorAll(`input[data-role="${roleName}"]`).length;
const grantedPerms = document.querySelectorAll(`input[data-role="${roleName}"]:checked`).length;
document.getElementById(`total-permissions-${roleName}`).textContent = totalPerms;
document.getElementById(`granted-permissions-${roleName}`).textContent = grantedPerms;
document.getElementById(`denied-permissions-${roleName}`).textContent = totalPerms - grantedPerms;
// Update page and section stats
updatePageStats(roleName);
}
function updatePageStats(roleName) {
const pages = {{ pages.keys()|list|tojson }};
pages.forEach(pageKey => {
const pageCheckboxes = document.querySelectorAll(`input[data-role="${roleName}"][data-permission^="${pageKey}."]`);
const pageGranted = Array.from(pageCheckboxes).filter(cb => cb.checked).length;
const pageTotal = pageCheckboxes.length;
const pageStatsEl = document.getElementById(`page-stats-${roleName}-${pageKey}`);
if (pageStatsEl) {
pageStatsEl.textContent = `${pageGranted}/${pageTotal}`;
}
// Update section stats within this page
const sections = {{ pages|tojson }};
Object.keys(sections[pageKey].sections).forEach(sectionKey => {
const sectionCheckboxes = document.querySelectorAll(`input[data-role="${roleName}"][data-permission^="${pageKey}.${sectionKey}."]`);
const sectionGranted = Array.from(sectionCheckboxes).filter(cb => cb.checked).length;
const sectionTotal = sectionCheckboxes.length;
const sectionStatsEl = document.getElementById(`section-stats-${roleName}-${pageKey}-${sectionKey}`);
if (sectionStatsEl) {
sectionStatsEl.textContent = `${sectionGranted}/${sectionTotal}`;
}
});
});
}
function savePermissions(roleName) {
const permissions = rolePermissions[roleName] || [];
fetch('/settings/save_role_permissions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role: roleName,
permissions: permissions
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Permissions saved successfully!');
} else {
alert('Error saving permissions: ' + data.error);
}
})
.catch(error => {
alert('Error saving permissions: ' + error);
});
}
function resetToDefaults(roleName) {
if (confirm('Are you sure you want to reset permissions to defaults? This will overwrite all current settings for this role.')) {
fetch('/settings/reset_role_permissions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role: roleName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
rolePermissions[roleName] = data.permissions;
loadPermissions(roleName);
alert('Permissions reset to defaults!');
} else {
alert('Error resetting permissions: ' + data.error);
}
})
.catch(error => {
alert('Error resetting permissions: ' + error);
});
}
}
function copyFromRole(targetRole) {
const roles = Object.keys(rolePermissions);
const sourceRole = prompt(`Enter the role to copy permissions from:\nAvailable roles: ${roles.join(', ')}`);
if (sourceRole && roles.includes(sourceRole) && sourceRole !== targetRole) {
if (confirm(`Copy all permissions from ${sourceRole} to ${targetRole}?`)) {
rolePermissions[targetRole] = [...(rolePermissions[sourceRole] || [])];
loadPermissions(targetRole);
alert('Permissions copied successfully!');
}
} else if (sourceRole) {
alert('Invalid role name or same as target role.');
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadPermissions(currentRole);
});
</script>
{% endblock %}