579 lines
29 KiB
HTML
579 lines
29 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}User Management{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<div class="container">
|
|
<ul class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">User Management</li>
|
|
</ul>
|
|
|
|
<div class="row align-items-center mb-3">
|
|
<div class="col">
|
|
<h1 class="page-header">User Management</h1>
|
|
<p class="text-muted">Manage system users, roles, and permissions</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createUserModal">
|
|
<i class="fa fa-plus me-2"></i>Add User
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="exportUsers()">
|
|
<i class="fa fa-download me-2"></i>Export
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="bulkActions()">
|
|
<i class="fa fa-cogs me-2"></i>Bulk Actions
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Statistics -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-primary bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-users fa-2x text-primary"></i>
|
|
</div>
|
|
<h5>Total Users</h5>
|
|
<div class="fs-24px fw-600 text-primary">{{ total_users }}</div>
|
|
<div class="text-muted small">All system users</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-success bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-user-check fa-2x text-success"></i>
|
|
</div>
|
|
<h5>Active Users</h5>
|
|
<div class="fs-24px fw-600 text-success">{{ active_users }}</div>
|
|
<div class="text-muted small">Currently active</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-warning bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-user-clock fa-2x text-warning"></i>
|
|
</div>
|
|
<h5>Pending Users</h5>
|
|
<div class="fs-24px fw-600 text-warning">{{ pending_users }}</div>
|
|
<div class="text-muted small">Awaiting approval</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-info bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-user-tie fa-2x text-info"></i>
|
|
</div>
|
|
<h5>Administrators</h5>
|
|
<div class="fs-24px fw-600 text-info">{{ admin_users }}</div>
|
|
<div class="text-muted small">System admins</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3 align-items-end">
|
|
<div class="col-md-3">
|
|
<label class="form-label">Search</label>
|
|
<input type="text" name="search" class="form-control" placeholder="Name, email, or username" value="{{ request.GET.search }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Department</label>
|
|
<select name="department" class="form-select">
|
|
<option value="">All Departments</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}" {% if request.GET.department == dept.id|stringformat:"s" %}selected{% endif %}>{{ dept.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Role</label>
|
|
<select name="role" class="form-select">
|
|
<option value="">All Roles</option>
|
|
{% for role in roles %}
|
|
<option value="{{ role.id }}" {% if request.GET.role == role.id|stringformat:"s" %}selected{% endif %}>{{ role.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="">All Status</option>
|
|
<option value="active" {% if request.GET.status == 'active' %}selected{% endif %}>Active</option>
|
|
<option value="inactive" {% if request.GET.status == 'inactive' %}selected{% endif %}>Inactive</option>
|
|
<option value="pending" {% if request.GET.status == 'pending' %}selected{% endif %}>Pending</option>
|
|
<option value="suspended" {% if request.GET.status == 'suspended' %}selected{% endif %}>Suspended</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fa fa-search me-2"></i>Filter
|
|
</button>
|
|
<a href="{% url 'core:user_management' %}" class="btn btn-outline-secondary ms-2">
|
|
<i class="fa fa-times me-2"></i>Clear
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Table -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">System Users</h4>
|
|
<div class="card-toolbar">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="selectAll">
|
|
<label class="form-check-label" for="selectAll">Select All</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped" id="usersTable">
|
|
<thead>
|
|
<tr>
|
|
<th width="30">
|
|
<input type="checkbox" class="form-check-input" id="selectAllHeader">
|
|
</th>
|
|
<th>User</th>
|
|
<th>Department</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th>Last Login</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in users %}
|
|
<tr>
|
|
<td>
|
|
<input type="checkbox" class="form-check-input user-checkbox" value="{{ user.id }}">
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="w-40px h-40px bg-{{ user.avatar_color|default:'primary' }} bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle me-3">
|
|
{% if user.avatar %}
|
|
<img src="{{ user.avatar.url }}" class="w-40px h-40px rounded-circle" alt="">
|
|
{% else %}
|
|
<i class="fa fa-user text-{{ user.avatar_color|default:'primary' }}"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{{ user.get_full_name|default:user.username }}</div>
|
|
<div class="text-muted small">{{ user.email }}</div>
|
|
{% if user.phone %}
|
|
<div class="text-muted small">{{ user.phone }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if user.department %}
|
|
<span class="badge bg-info">{{ user.department.name }}</span>
|
|
{% else %}
|
|
<span class="text-muted">Not assigned</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% for role in user.roles.all %}
|
|
<span class="badge bg-secondary me-1">{{ role.name }}</span>
|
|
{% empty %}
|
|
<span class="text-muted">No roles</span>
|
|
{% endfor %}
|
|
</td>
|
|
<td>
|
|
{% if user.is_active %}
|
|
{% if user.is_online %}
|
|
<span class="badge bg-success">
|
|
<i class="fa fa-circle me-1"></i>Online
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-primary">Active</span>
|
|
{% endif %}
|
|
{% elif user.is_pending %}
|
|
<span class="badge bg-warning">Pending</span>
|
|
{% elif user.is_suspended %}
|
|
<span class="badge bg-danger">Suspended</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if user.last_login %}
|
|
<div>{{ user.last_login|date:"M d, Y" }}</div>
|
|
<div class="text-muted small">{{ user.last_login|date:"g:i A" }}</div>
|
|
{% else %}
|
|
<span class="text-muted">Never</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div>{{ user.date_joined|date:"M d, Y" }}</div>
|
|
<div class="text-muted small">{{ user.created_by.get_full_name|default:user.created_by.username }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="viewUser('{{ user.id }}')">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="editUser('{{ user.id }}')">
|
|
<i class="fa fa-edit"></i>
|
|
</button>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fa fa-cog"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="resetPassword('{{ user.id }}')">
|
|
<i class="fa fa-key me-2"></i>Reset Password
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="toggleStatus('{{ user.id }}')">
|
|
<i class="fa fa-toggle-on me-2"></i>Toggle Status
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="viewLoginHistory('{{ user.id }}')">
|
|
<i class="fa fa-history me-2"></i>Login History
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="#" onclick="deleteUser('{{ user.id }}')">
|
|
<i class="fa fa-trash me-2"></i>Delete User
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<nav aria-label="Users pagination">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.department %}&department={{ request.GET.department }}{% endif %}{% if request.GET.role %}&role={{ request.GET.role }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.department %}&department={{ request.GET.department }}{% endif %}{% if request.GET.role %}&role={{ request.GET.role }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for num in page_obj.paginator.page_range %}
|
|
{% if page_obj.number == num %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ num }}</span>
|
|
</li>
|
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ num }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.department %}&department={{ request.GET.department }}{% endif %}{% if request.GET.role %}&role={{ request.GET.role }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">{{ num }}</a>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.department %}&department={{ request.GET.department }}{% endif %}{% if request.GET.role %}&role={{ request.GET.role }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.department %}&department={{ request.GET.department }}{% endif %}{% if request.GET.role %}&role={{ request.GET.role }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div class="modal fade" id="createUserModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Create New User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="createUserForm">
|
|
<div class="modal-body">
|
|
{% csrf_token %}
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">First Name *</label>
|
|
<input type="text" name="first_name" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Last Name *</label>
|
|
<input type="text" name="last_name" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Username *</label>
|
|
<input type="text" name="username" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Email *</label>
|
|
<input type="email" name="email" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Phone</label>
|
|
<input type="tel" name="phone" class="form-control">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Department</label>
|
|
<select name="department" class="form-select">
|
|
<option value="">Select Department</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Role</label>
|
|
<select name="roles" class="form-select" multiple>
|
|
{% for role in roles %}
|
|
<option value="{{ role.id }}">{{ role.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="active">Active</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Password *</label>
|
|
<input type="password" name="password" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Confirm Password *</label>
|
|
<input type="password" name="password_confirm" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="send_welcome_email" checked>
|
|
<label class="form-check-label">Send welcome email to user</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize DataTable
|
|
$('#usersTable').DataTable({
|
|
responsive: true,
|
|
pageLength: 25,
|
|
order: [[1, 'asc']],
|
|
columnDefs: [
|
|
{ orderable: false, targets: [0, 7] }
|
|
]
|
|
});
|
|
|
|
// Initialize Select2
|
|
$('select[multiple]').select2({
|
|
placeholder: 'Select roles',
|
|
allowClear: true
|
|
});
|
|
|
|
// Select all functionality
|
|
$('#selectAllHeader, #selectAll').change(function() {
|
|
$('.user-checkbox').prop('checked', this.checked);
|
|
});
|
|
|
|
$('.user-checkbox').change(function() {
|
|
var allChecked = $('.user-checkbox:checked').length === $('.user-checkbox').length;
|
|
$('#selectAllHeader, #selectAll').prop('checked', allChecked);
|
|
});
|
|
|
|
// Create user form submission
|
|
$('#createUserForm').submit(function(e) {
|
|
e.preventDefault();
|
|
|
|
var formData = new FormData(this);
|
|
|
|
$.post('{% url "core:create_user" %}', formData, function(response) {
|
|
if (response.success) {
|
|
$('#createUserModal').modal('hide');
|
|
toastr.success('User created successfully');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to create user: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to create user');
|
|
});
|
|
});
|
|
});
|
|
|
|
function viewUser(userId) {
|
|
window.open('{% url "accounts:user_detail" 0 %}'.replace('0', userId), '_blank');
|
|
}
|
|
|
|
function editUser(userId) {
|
|
window.location.href = '{% url "accounts:user_edit" 0 %}'.replace('0', userId);
|
|
}
|
|
|
|
function resetPassword(userId) {
|
|
if (confirm('Reset password for this user? They will receive an email with instructions.')) {
|
|
$.post('{% url "core:reset_user_password" %}', {
|
|
'user_id': userId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('Password reset email sent');
|
|
} else {
|
|
toastr.error('Failed to reset password: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to reset password');
|
|
});
|
|
}
|
|
}
|
|
|
|
function toggleStatus(userId) {
|
|
$.post('{% url "core:toggle_user_status" %}', {
|
|
'user_id': userId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('User status updated');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to update status: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to update status');
|
|
});
|
|
}
|
|
|
|
function viewLoginHistory(userId) {
|
|
window.open('{% url "core:user_login_history" 0 %}'.replace('0', userId), '_blank');
|
|
}
|
|
|
|
function deleteUser(userId) {
|
|
if (confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
|
$.post('{% url "core:delete_user" %}', {
|
|
'user_id': userId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('User deleted successfully');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to delete user: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to delete user');
|
|
});
|
|
}
|
|
}
|
|
|
|
function exportUsers() {
|
|
var params = new URLSearchParams(window.location.search);
|
|
params.set('export', 'csv');
|
|
window.location.href = '{% url "core:user_management" %}?' + params.toString();
|
|
}
|
|
|
|
function bulkActions() {
|
|
var selectedUsers = $('.user-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedUsers.length === 0) {
|
|
toastr.warning('Please select users first');
|
|
return;
|
|
}
|
|
|
|
// Show bulk actions modal or dropdown
|
|
var action = prompt('Enter action (activate, deactivate, delete):');
|
|
if (action) {
|
|
$.post('{% url "core:bulk_user_actions" %}', {
|
|
'user_ids': selectedUsers,
|
|
'action': action,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('Bulk action completed');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to perform bulk action: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to perform bulk action');
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|