393 lines
21 KiB
HTML
393 lines
21 KiB
HTML
{% extends 'base.html' %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "User & Role Management" %} - {{ site_settings.business_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2 class="h4 mb-0 fw-bold">{% trans "User & Role Management" %}</h2>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addGroupModal">
|
|
<i class="bi bi-shield-lock me-2"></i> {% trans "Add New Group" %}
|
|
</button>
|
|
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addUserModal">
|
|
<i class="bi bi-person-plus me-2"></i> {% trans "Add New User" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Nav Tabs -->
|
|
<ul class="nav nav-pills mb-4 gap-2" id="managementTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active rounded-pill px-4 fw-bold shadow-sm" id="users-tab" data-bs-toggle="pill" data-bs-target="#users" type="button" role="tab">
|
|
<i class="bi bi-people me-2"></i> {% trans "Users" %}
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link rounded-pill px-4 fw-bold shadow-sm" id="groups-tab" data-bs-toggle="pill" data-bs-target="#groups" type="button" role="tab">
|
|
<i class="bi bi-shield-check me-2"></i> {% trans "Groups & Permissions" %}
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content" id="managementTabsContent">
|
|
<!-- Users Tab -->
|
|
<div class="tab-pane fade show active" id="users" role="tabpanel">
|
|
<div class="card border-0 shadow-sm rounded-4">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light text-muted small text-uppercase fw-bold">
|
|
<tr>
|
|
<th class="ps-4">{% trans "User" %}</th>
|
|
<th>{% trans "Email" %}</th>
|
|
<th>{% trans "Groups" %}</th>
|
|
<th>{% trans "Status" %}</th>
|
|
<th>{% trans "Last Login" %}</th>
|
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for u in users %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary-subtle text-primary rounded-circle p-2 me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
|
<i class="bi bi-person fs-5"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{{ u.username }}</div>
|
|
{% if u.is_superuser %}<span class="badge bg-danger-subtle text-danger small">Superuser</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>{{ u.email|default:"-" }}</td>
|
|
<td>
|
|
{% for group in u.groups.all %}
|
|
<span class="badge bg-info-subtle text-info">{{ group.name }}</span>
|
|
{% empty %}
|
|
<span class="text-muted small">No Role</span>
|
|
{% endfor %}
|
|
</td>
|
|
<td>
|
|
{% if u.is_active %}
|
|
<span class="badge bg-success-subtle text-success">{% trans "Active" %}</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary-subtle text-secondary">{% trans "Inactive" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ u.last_login|date:"Y-m-d H:i"|default:"Never" }}</td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-outline-primary edit-user-btn"
|
|
data-id="{{ u.id }}"
|
|
data-username="{{ u.username }}"
|
|
data-email="{{ u.email }}"
|
|
data-groups='[{% for g in u.groups.all %}"{{ g.id }}"{% if not forloop.last %},{% endif %}{% endfor %}]'
|
|
data-bs-toggle="modal" data-bs-target="#editUserModal">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<form action="{% url 'user_management' %}" method="post" class="d-inline">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="toggle_status">
|
|
<input type="hidden" name="user_id" value="{{ u.id }}">
|
|
<button type="submit" class="btn btn-sm {% if u.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
|
{% if u == user %}disabled title="Cannot deactivate yourself"{% endif %}>
|
|
{% if u.is_active %}<i class="bi bi-person-x"></i>{% else %}<i class="bi bi-person-check"></i>{% endif %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Groups Tab -->
|
|
<div class="tab-pane fade" id="groups" role="tabpanel">
|
|
<div class="card border-0 shadow-sm rounded-4">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light text-muted small text-uppercase fw-bold">
|
|
<tr>
|
|
<th class="ps-4">{% trans "Group Name" %}</th>
|
|
<th>{% trans "Permissions Count" %}</th>
|
|
<th>{% trans "Users Count" %}</th>
|
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in groups %}
|
|
<tr>
|
|
<td class="ps-4 fw-bold text-primary">{{ g.name }}</td>
|
|
<td>
|
|
<span class="badge bg-secondary-subtle text-secondary">{{ g.permissions.count }} {% trans "Permissions" %}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info-subtle text-info">{{ g.user_set.count }} {% trans "Users" %}</span>
|
|
</td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-outline-primary edit-group-btn"
|
|
data-id="{{ g.id }}"
|
|
data-name="{{ g.name }}"
|
|
data-bs-toggle="modal" data-bs-target="#editGroupModal">
|
|
<i class="bi bi-shield-lock"></i> Edit Perms
|
|
</button>
|
|
<form action="{% url 'user_management' %}" method="post" class="d-inline" onsubmit="return confirm('{% trans "Are you sure you want to delete this group?" %}');">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="delete_group">
|
|
<input type="hidden" name="group_id" value="{{ g.id }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add User Modal -->
|
|
<div class="modal fade" id="addUserModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg rounded-4">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title fw-bold">{% trans "Create New User" %}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form action="{% url 'user_management' %}" method="post">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="add">
|
|
<div class="modal-body py-4">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Username" %}</label>
|
|
<input type="text" name="username" class="form-control rounded-3" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Email" %}</label>
|
|
<input type="email" name="email" class="form-control rounded-3">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Password" %}</label>
|
|
<input type="password" name="password" class="form-control rounded-3" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Assign Groups" %}</label>
|
|
<select name="groups" class="form-select rounded-3" multiple style="height: 120px;">
|
|
{% for g in groups %}
|
|
<option value="{{ g.id }}">{{ g.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<div class="form-text small text-muted">Hold Ctrl/Cmd to select multiple groups.</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Create User" %}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div class="modal fade" id="editUserModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg rounded-4">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title fw-bold">{% trans "Edit User" %}: <span id="editUserTitle"></span></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form action="{% url 'user_management' %}" method="post">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="edit_user">
|
|
<input type="hidden" name="user_id" id="editUserId">
|
|
<div class="modal-body py-4">
|
|
<div class="mb-3 text-muted small">Username: <strong id="editUserUsername"></strong></div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Email" %}</label>
|
|
<input type="email" name="email" id="editUserEmail" class="form-control rounded-3">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "New Password" %}</label>
|
|
<input type="password" name="password" class="form-control rounded-3" placeholder="Leave blank to keep current">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Assign Groups" %}</label>
|
|
<select name="groups" id="editUserGroups" class="form-select rounded-3" multiple style="height: 120px;">
|
|
{% for g in groups %}
|
|
<option value="{{ g.id }}">{{ g.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Changes" %}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Group Modal -->
|
|
<div class="modal fade" id="addGroupModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg rounded-4">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title fw-bold">{% trans "Create New Role/Group" %}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form action="{% url 'user_management' %}" method="post">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="add_group">
|
|
<div class="modal-body py-4">
|
|
<div class="mb-4">
|
|
<label class="form-label small fw-bold">{% trans "Group Name" %}</label>
|
|
<input type="text" name="name" class="form-control rounded-3" required placeholder="e.g., Inventory Manager">
|
|
</div>
|
|
<h6 class="fw-bold mb-3 small text-uppercase text-muted">{% trans "Assign Permissions" %}</h6>
|
|
<div class="row g-3" style="max-height: 400px; overflow-y: auto;">
|
|
{% for perm in permissions %}
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="form-check card p-2 border-light shadow-none">
|
|
<input class="form-check-input ms-0 me-2" type="checkbox" name="permissions" value="{{ perm.id }}" id="perm_{{ perm.id }}">
|
|
<label class="form-check-label small d-block" for="perm_{{ perm.id }}">
|
|
<span class="text-primary fw-bold">{{ perm.content_type.app_label }}</span> | {{ perm.name }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Create Group" %}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Group Modal -->
|
|
<div class="modal fade" id="editGroupModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg rounded-4">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title fw-bold">{% trans "Edit Role/Group" %}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form action="{% url 'user_management' %}" method="post">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="edit_group">
|
|
<input type="hidden" name="group_id" id="editGroupId">
|
|
<div class="modal-body py-4">
|
|
<div class="mb-4">
|
|
<label class="form-label small fw-bold">{% trans "Group Name" %}</label>
|
|
<input type="text" name="name" id="editGroupName" class="form-control rounded-3" required>
|
|
</div>
|
|
<h6 class="fw-bold mb-3 small text-uppercase text-muted">{% trans "Permissions" %}</h6>
|
|
<div class="row g-3" style="max-height: 400px; overflow-y: auto;">
|
|
{% for perm in permissions %}
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="form-check card p-2 border-light shadow-none">
|
|
<input class="form-check-input edit-group-perm ms-0 me-2" type="checkbox" name="permissions" value="{{ perm.id }}" id="edit_perm_{{ perm.id }}">
|
|
<label class="form-check-label small d-block" for="edit_perm_{{ perm.id }}">
|
|
<span class="text-primary fw-bold">{{ perm.content_type.app_label }}</span> | {{ perm.name }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
|
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Changes" %}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Tab Persistence
|
|
const hash = window.location.hash;
|
|
if (hash) {
|
|
const triggerEl = document.querySelector('#managementTabs button[data-bs-target="' + hash + '"]');
|
|
if (triggerEl) {
|
|
bootstrap.Tab.getOrCreateInstance(triggerEl).show();
|
|
}
|
|
}
|
|
|
|
const tabButtons = document.querySelectorAll('#managementTabs button');
|
|
tabButtons.forEach(button => {
|
|
button.addEventListener('shown.bs.tab', event => {
|
|
window.location.hash = event.target.getAttribute('data-bs-target');
|
|
});
|
|
});
|
|
|
|
// Edit User Modal Population
|
|
const editUserBtns = document.querySelectorAll('.edit-user-btn');
|
|
editUserBtns.forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const id = this.dataset.id;
|
|
const username = this.dataset.username;
|
|
const email = this.dataset.email;
|
|
const groups = JSON.parse(this.dataset.groups);
|
|
|
|
document.getElementById('editUserId').value = id;
|
|
document.getElementById('editUserTitle').innerText = username;
|
|
document.getElementById('editUserUsername').innerText = username;
|
|
document.getElementById('editUserEmail').value = email;
|
|
|
|
const groupSelect = document.getElementById('editUserGroups');
|
|
Array.from(groupSelect.options).forEach(option => {
|
|
option.selected = groups.includes(option.value);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Edit Group Modal Population
|
|
const editGroupBtns = document.querySelectorAll('.edit-group-btn');
|
|
editGroupBtns.forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const id = this.dataset.id;
|
|
const name = this.dataset.name;
|
|
|
|
document.getElementById('editGroupId').value = id;
|
|
document.getElementById('editGroupName').value = name;
|
|
|
|
// Reset checkboxes
|
|
document.querySelectorAll('.edit-group-perm').forEach(cb => cb.checked = false);
|
|
|
|
// Fetch group permissions
|
|
fetch(`/api/group-details/${id}/`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.permissions) {
|
|
data.permissions.forEach(permId => {
|
|
const cb = document.getElementById(`edit_perm_${permId}`);
|
|
if (cb) cb.checked = true;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |