2103 lines
52 KiB
HTML
2103 lines
52 KiB
HTML
{% extends "base_authenticated.html" %} {% block title %}User Management - QR Code
|
|
Management{% endblock %} {% block content %}
|
|
<div class="users-page">
|
|
<div class="users-header">
|
|
<div class="header-content">
|
|
<h1>
|
|
<i class="fas fa-users"></i>
|
|
User Management
|
|
</h1>
|
|
<p>Manage system users and their access permissions</p>
|
|
</div>
|
|
|
|
<div class="header-actions">
|
|
<a href="{{ url_for('create_user') }}" class="btn btn-primary">
|
|
<i class="fas fa-user-plus"></i>
|
|
Add New User
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Statistics -->
|
|
<div class="users-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-icon admin">
|
|
<i class="fas fa-crown"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>
|
|
{{ users|selectattr('role', 'equalto',
|
|
'admin')|selectattr('active_status', 'equalto', True)|list|length }}
|
|
</h3>
|
|
<p>Active Administrators</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon staff">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>
|
|
{{ users|selectattr('role', 'equalto',
|
|
'staff')|selectattr('active_status', 'equalto', True)|list|length }}
|
|
</h3>
|
|
<p>Active Staff Members</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon active">
|
|
<i class="fas fa-user-check"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>
|
|
{{ users|selectattr('active_status', 'equalto', True)|list|length }}
|
|
</h3>
|
|
<p>Total Active Users</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon inactive">
|
|
<i class="fas fa-user-times"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>
|
|
{{ users|selectattr('active_status', 'equalto', False)|list|length }}
|
|
</h3>
|
|
<p>Inactive Users</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters and Search -->
|
|
<div class="users-controls">
|
|
<div class="search-filters">
|
|
<div class="search-box">
|
|
<i class="fas fa-search"></i>
|
|
<input
|
|
type="text"
|
|
id="searchUsers"
|
|
placeholder="Search users by name, email, or username..."
|
|
/>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<select id="roleFilter" class="filter-select">
|
|
<option value="">All Roles</option>
|
|
<option value="admin">Administrators</option>
|
|
<option value="staff">Staff Members</option>
|
|
</select>
|
|
|
|
<select id="statusFilter" class="filter-select">
|
|
<option value="">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="view-options">
|
|
<button class="btn btn-secondary btn-sm" onclick="refreshUserList()">
|
|
<i class="fas fa-sync-alt"></i>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Table -->
|
|
<div class="users-container">
|
|
<div class="users-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>User Information</th>
|
|
<th>Contact</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th>QR Codes</th>
|
|
<th>Created</th>
|
|
<th>Last Login</th>
|
|
<th style="width: 200px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="usersTableBody">
|
|
{% for user in users %}
|
|
<tr
|
|
class="user-row"
|
|
data-name="{{ user.full_name.lower() }}"
|
|
data-email="{{ user.email.lower() }}"
|
|
data-username="{{ user.username.lower() }}"
|
|
data-role="{{ user.role }}"
|
|
data-status="{{ 'active' if user.active_status else 'inactive' }}"
|
|
data-user-id="{{ user.id }}"
|
|
>
|
|
<td>{{ loop.index }}</td>
|
|
|
|
<!-- User Information -->
|
|
<td>
|
|
<div class="user-info">
|
|
<div class="user-avatar">
|
|
<i class="fas fa-user"></i>
|
|
{% if user.role == 'admin' %}
|
|
<div class="avatar-badge admin">
|
|
<i class="fas fa-crown"></i>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="user-details">
|
|
<div class="user-name">{{ user.full_name }}</div>
|
|
<div class="user-username">@{{ user.username }}</div>
|
|
{% if user.id == session.user_id %}
|
|
<span class="current-user-badge">You</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Contact Information -->
|
|
<td>
|
|
<div class="contact-info">
|
|
<div class="user-email">
|
|
<i class="fas fa-envelope"></i>
|
|
{{ user.email }}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Role -->
|
|
<td>
|
|
<span class="role-badge {{ user.role }}">
|
|
<i
|
|
class="fas {{ 'fa-crown' if user.role == 'admin' else 'fa-user-tie' }}"
|
|
></i>
|
|
{{ user.role.title() }}
|
|
</span>
|
|
</td>
|
|
|
|
<!-- Status -->
|
|
<td>
|
|
<span
|
|
class="status-badge {{ 'active' if user.active_status else 'inactive' }}"
|
|
>
|
|
<i
|
|
class="fas {{ 'fa-check-circle' if user.active_status else 'fa-times-circle' }}"
|
|
></i>
|
|
{{ 'Active' if user.active_status else 'Inactive' }}
|
|
</span>
|
|
</td>
|
|
|
|
<!-- QR Codes Count -->
|
|
<td>
|
|
<div class="qr-count">
|
|
<div class="qr-total">
|
|
<strong>{{ user.created_qr_codes.count() }}</strong>
|
|
<small>total</small>
|
|
</div>
|
|
<div class="qr-active">
|
|
<span
|
|
>{{
|
|
user.created_qr_codes.filter_by(active_status=True).count()
|
|
}}</span
|
|
>
|
|
<small>active</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Created Date -->
|
|
<td>
|
|
<div class="date-info">
|
|
<div class="date">
|
|
{{ user.created_date.strftime('%Y-%m-%d') }}
|
|
</div>
|
|
<div class="time">
|
|
{{ user.created_date.strftime('%H:%M') }}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Last Login -->
|
|
<td>
|
|
{% if user.last_login_date %}
|
|
<div class="date-info">
|
|
<div class="date">
|
|
{{ user.last_login_date.strftime('%Y-%m-%d') }}
|
|
</div>
|
|
<div class="time">
|
|
{{ user.last_login_date.strftime('%H:%M') }}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">
|
|
<i class="fas fa-minus-circle"></i>
|
|
Never
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
|
|
<!-- Actions -->
|
|
<td>
|
|
<div class="user-actions">
|
|
<!-- Edit Button -->
|
|
<a
|
|
href="{{ url_for('edit_user', user_id=user.id) }}"
|
|
class="action-btn btn-edit"
|
|
title="Edit User"
|
|
>
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
|
|
<!-- Role Actions -->
|
|
{% if user.role == 'staff' %}
|
|
<a
|
|
href="{{ url_for('promote_user', user_id=user.id) }}"
|
|
class="action-btn btn-promote"
|
|
title="Promote to Admin"
|
|
onclick="return confirmPromoteUser('{{ user.full_name }}')"
|
|
>
|
|
<i class="fas fa-arrow-up"></i>
|
|
</a>
|
|
{% elif user.role == 'admin' and user.id != session.user_id %}
|
|
<a
|
|
href="{{ url_for('demote_user', user_id=user.id) }}"
|
|
class="action-btn btn-demote"
|
|
title="Demote to Staff"
|
|
onclick="return confirmDemoteUser('{{ user.full_name }}')"
|
|
>
|
|
<i class="fas fa-arrow-down"></i>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<!-- Status Actions -->
|
|
{% if user.active_status %} {% if user.id != session.user_id %}
|
|
<a
|
|
href="{{ url_for('delete_user', user_id=user.id) }}"
|
|
class="action-btn btn-deactivate"
|
|
title="Deactivate User"
|
|
onclick="return confirmDeactivateUser('{{ user.full_name }}', '{{ user.role }}')"
|
|
>
|
|
<i class="fas fa-user-times"></i>
|
|
</a>
|
|
{% else %}
|
|
<span
|
|
class="action-btn btn-disabled"
|
|
title="Cannot deactivate yourself"
|
|
>
|
|
<i class="fas fa-lock"></i>
|
|
</span>
|
|
{% endif %} {% else %}
|
|
<a
|
|
href="{{ url_for('reactivate_user', user_id=user.id) }}"
|
|
class="action-btn btn-activate"
|
|
title="Reactivate User"
|
|
onclick="return confirm('Reactivate {{ user.full_name }}?')"
|
|
>
|
|
<i class="fas fa-user-check"></i>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<!-- Permanent Delete Action (Only for inactive users) -->
|
|
{% if not user.active_status and user.id != session.user_id %}
|
|
<a
|
|
href="{{ url_for('permanently_delete_user', user_id=user.id) }}"
|
|
class="action-btn btn-delete"
|
|
title="Permanently Delete User"
|
|
onclick="return confirmPermanentDelete('{{ user.full_name }}')"
|
|
>
|
|
<i class="fas fa-trash"></i>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<!-- More Options Dropdown -->
|
|
<div class="dropdown">
|
|
<button
|
|
class="action-btn btn-more dropdown-toggle"
|
|
title="More Options"
|
|
onclick="toggleDropdown(event, this)"
|
|
>
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<button
|
|
onclick="viewUserDetails({{ user.id }})"
|
|
class="dropdown-item"
|
|
>
|
|
<i class="fas fa-eye"></i> View Details
|
|
</button>
|
|
{% if user.id != session.user_id %}
|
|
<button
|
|
onclick="resetUserPassword({{ user.id }}, '{{ user.full_name }}')"
|
|
class="dropdown-item"
|
|
>
|
|
<i class="fas fa-key"></i> Reset Password
|
|
</button>
|
|
<div class="dropdown-divider"></div>
|
|
<button
|
|
onclick="viewUserQRCodes({{ user.id }})"
|
|
class="dropdown-item"
|
|
>
|
|
<i class="fas fa-qrcode"></i> View QR Codes
|
|
</button>
|
|
{% if not user.active_status %}
|
|
<div class="dropdown-divider"></div>
|
|
<a
|
|
href="{{ url_for('permanently_delete_user', user_id=user.id) }}"
|
|
class="dropdown-item danger-item"
|
|
onclick="return confirmPermanentDelete('{{ user.full_name }}')"
|
|
>
|
|
<i class="fas fa-trash"></i> Permanently Delete
|
|
</a>
|
|
{% endif %} {% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %} {% if not users %}
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; padding: 3rem">
|
|
<div class="empty-icon">
|
|
<i
|
|
class="fas fa-users"
|
|
style="
|
|
font-size: 3rem;
|
|
color: var(--gray-400);
|
|
margin-bottom: 1rem;
|
|
"
|
|
></i>
|
|
</div>
|
|
<h3 style="color: var(--gray-900); margin-bottom: 0.5rem">
|
|
No Users Found
|
|
</h3>
|
|
<p style="color: var(--gray-500); margin-bottom: 1rem">
|
|
There are no users in the system yet.
|
|
</p>
|
|
<a href="{{ url_for('create_user') }}" class="btn btn-primary">
|
|
<i class="fas fa-user-plus"></i>
|
|
Create First User
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State for Filtered Results -->
|
|
<div class="empty-state" id="emptyState" style="display: none">
|
|
<div class="empty-icon">
|
|
<i class="fas fa-search"></i>
|
|
</div>
|
|
<h3>No Users Found</h3>
|
|
<p>Try adjusting your search criteria or filters</p>
|
|
<button onclick="clearFilters()" class="btn btn-secondary">
|
|
<i class="fas fa-refresh"></i>
|
|
Clear Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Details Modal -->
|
|
<div id="userDetailsModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="userModalTitle">User Details</h3>
|
|
<button class="modal-close" onclick="closeUserDetailsModal()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body" id="userModalBody">
|
|
<div class="loading-content">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
Loading user details...
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button onclick="closeUserDetailsModal()" class="btn btn-secondary">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password Reset Modal -->
|
|
<div id="passwordResetModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Reset User Password</h3>
|
|
<button class="modal-close" onclick="closePasswordResetModal()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>
|
|
This feature would typically integrate with your password reset system.
|
|
</p>
|
|
<p>
|
|
For security, consider implementing email-based password reset instead
|
|
of direct admin reset.
|
|
</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button onclick="closePasswordResetModal()" class="btn btn-secondary">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %} {% block extra_scripts %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
initializeUserManagement();
|
|
});
|
|
|
|
function initializeUserManagement() {
|
|
initializeSearch();
|
|
initializeFilters();
|
|
setupEventListeners();
|
|
}
|
|
|
|
// Search and Filter Functions
|
|
function initializeSearch() {
|
|
const searchInput = document.getElementById("searchUsers");
|
|
let searchTimeout;
|
|
|
|
if (searchInput) {
|
|
searchInput.addEventListener("input", function () {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
filterUsers();
|
|
}, 300);
|
|
});
|
|
}
|
|
}
|
|
|
|
function initializeFilters() {
|
|
const roleFilter = document.getElementById("roleFilter");
|
|
const statusFilter = document.getElementById("statusFilter");
|
|
|
|
if (roleFilter) roleFilter.addEventListener("change", filterUsers);
|
|
if (statusFilter) statusFilter.addEventListener("change", filterUsers);
|
|
}
|
|
|
|
function filterUsers() {
|
|
const searchTerm = document
|
|
.getElementById("searchUsers")
|
|
.value.toLowerCase();
|
|
const roleFilter = document.getElementById("roleFilter").value;
|
|
const statusFilter = document.getElementById("statusFilter").value;
|
|
const userRows = document.querySelectorAll(".user-row");
|
|
const emptyState = document.getElementById("emptyState");
|
|
const usersTable = document.querySelector(".users-table");
|
|
|
|
let visibleCount = 0;
|
|
|
|
userRows.forEach((row) => {
|
|
const name = row.dataset.name || "";
|
|
const email = row.dataset.email || "";
|
|
const username = row.dataset.username || "";
|
|
const role = row.dataset.role || "";
|
|
const status = row.dataset.status || "";
|
|
|
|
const matchesSearch =
|
|
!searchTerm ||
|
|
name.includes(searchTerm) ||
|
|
email.includes(searchTerm) ||
|
|
username.includes(searchTerm);
|
|
const matchesRole = !roleFilter || role === roleFilter;
|
|
const matchesStatus = !statusFilter || status === statusFilter;
|
|
|
|
if (matchesSearch && matchesRole && matchesStatus) {
|
|
row.style.display = "";
|
|
visibleCount++;
|
|
} else {
|
|
row.style.display = "none";
|
|
}
|
|
});
|
|
|
|
// Show/hide empty state
|
|
if (visibleCount === 0 && (searchTerm || roleFilter || statusFilter)) {
|
|
if (emptyState) emptyState.style.display = "block";
|
|
if (usersTable) usersTable.style.display = "none";
|
|
} else {
|
|
if (emptyState) emptyState.style.display = "none";
|
|
if (usersTable) usersTable.style.display = "block";
|
|
}
|
|
}
|
|
|
|
function clearFilters() {
|
|
document.getElementById("searchUsers").value = "";
|
|
document.getElementById("roleFilter").value = "";
|
|
document.getElementById("statusFilter").value = "";
|
|
filterUsers();
|
|
}
|
|
|
|
function refreshUserList() {
|
|
window.location.reload();
|
|
}
|
|
|
|
// Event Listeners
|
|
function setupEventListeners() {
|
|
// Close dropdowns when clicking outside
|
|
document.addEventListener("click", function (e) {
|
|
if (!e.target.closest(".dropdown")) {
|
|
document.querySelectorAll(".dropdown-menu.show").forEach((menu) => {
|
|
menu.classList.remove("show");
|
|
});
|
|
}
|
|
});
|
|
|
|
// Close modals when clicking outside
|
|
const modals = document.querySelectorAll(".modal");
|
|
modals.forEach((modal) => {
|
|
modal.addEventListener("click", function (e) {
|
|
if (e.target === this) {
|
|
this.style.display = "none";
|
|
}
|
|
});
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener("keydown", function (e) {
|
|
// ESC to close modals
|
|
if (e.key === "Escape") {
|
|
closeUserDetailsModal();
|
|
closePasswordResetModal();
|
|
}
|
|
|
|
// Ctrl/Cmd + F to focus search
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
|
e.preventDefault();
|
|
const searchInput = document.getElementById("searchUsers");
|
|
if (searchInput) searchInput.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Dropdown Functions
|
|
function toggleDropdown(event, button) {
|
|
event.stopPropagation();
|
|
|
|
const dropdown = button.closest(".dropdown");
|
|
const menu = dropdown.querySelector(".dropdown-menu");
|
|
|
|
// Close all other dropdowns
|
|
document.querySelectorAll(".dropdown-menu").forEach((otherMenu) => {
|
|
if (otherMenu !== menu) {
|
|
otherMenu.classList.remove("show");
|
|
}
|
|
});
|
|
|
|
// Toggle current dropdown
|
|
menu.classList.toggle("show");
|
|
}
|
|
|
|
// Confirmation Functions
|
|
function confirmDeactivateUser(userName, userRole) {
|
|
let message = `Are you sure you want to deactivate ${userName}?\n\n`;
|
|
|
|
if (userRole === "admin") {
|
|
message += "This will:\n";
|
|
message += "• Remove admin privileges\n";
|
|
message += "• Disable their login access\n";
|
|
message += "• Preserve their data\n";
|
|
message += "• Allow reactivation later\n\n";
|
|
message +=
|
|
"Note: System will prevent deactivation if this is the last admin.";
|
|
} else {
|
|
message += "This will:\n";
|
|
message += "• Disable their login access\n";
|
|
message += "• Preserve their QR codes\n";
|
|
message += "• Allow reactivation later";
|
|
}
|
|
|
|
return confirm(message);
|
|
}
|
|
|
|
function confirmPromoteUser(userName) {
|
|
return confirm(
|
|
`Promote ${userName} to admin?\n\n` +
|
|
"This will give them full system access including:\n" +
|
|
"• User management\n" +
|
|
"• System settings\n" +
|
|
"• All QR code operations"
|
|
);
|
|
}
|
|
|
|
function confirmDemoteUser(userName) {
|
|
return confirm(
|
|
`Demote ${userName} from admin to staff?\n\n` +
|
|
"This will:\n" +
|
|
"• Remove admin privileges\n" +
|
|
"• Limit access to QR code management only\n" +
|
|
"• Preserve their account and data"
|
|
);
|
|
}
|
|
|
|
// Permanent Delete Functions
|
|
function confirmPermanentDelete(userName) {
|
|
return confirm(
|
|
`⚠️ PERMANENTLY DELETE ${userName}?\n\n` +
|
|
"This action CANNOT be undone and will:\n" +
|
|
"• Permanently remove the user account\n" +
|
|
"• Delete all associated QR codes\n" +
|
|
"• Remove all user data from database\n\n" +
|
|
"Are you absolutely sure?"
|
|
);
|
|
}
|
|
|
|
// Modal Functions
|
|
function viewUserDetails(userId) {
|
|
const modal = document.getElementById("userDetailsModal");
|
|
const modalTitle = document.getElementById("userModalTitle");
|
|
const modalBody = document.getElementById("userModalBody");
|
|
|
|
if (!modal || !modalTitle || !modalBody) return;
|
|
|
|
modalTitle.textContent = "User Details";
|
|
modalBody.innerHTML = `
|
|
<div class="loading-content">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
Loading user information...
|
|
</div>
|
|
`;
|
|
|
|
modal.style.display = "flex";
|
|
|
|
// Find user data from the table
|
|
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
|
|
if (userRow) {
|
|
const userName = userRow.querySelector(".user-name").textContent;
|
|
const userEmail = userRow
|
|
.querySelector(".user-email")
|
|
.textContent.replace(
|
|
/^\s*[\u2709\u2709\uFE0F\u2196\u2197\u2198\u2199]*\s*/,
|
|
""
|
|
)
|
|
.trim();
|
|
const userRole = userRow.dataset.role;
|
|
const userStatus = userRow.dataset.status;
|
|
const qrCount = userRow.querySelector(".qr-total strong").textContent;
|
|
const activeQrCount =
|
|
userRow.querySelector(".qr-active span").textContent;
|
|
|
|
setTimeout(() => {
|
|
modalTitle.textContent = `User Details - ${userName}`;
|
|
modalBody.innerHTML = `
|
|
<div class="user-detail-view">
|
|
<div class="detail-header">
|
|
<div class="detail-avatar">
|
|
<i class="fas fa-user"></i>
|
|
${
|
|
userRole === "admin"
|
|
? '<div class="avatar-badge admin"><i class="fas fa-crown"></i></div>'
|
|
: ""
|
|
}
|
|
</div>
|
|
<div class="detail-info">
|
|
<h3>${userName}</h3>
|
|
<p>${userEmail}</p>
|
|
<span class="role-badge ${userRole}">
|
|
<i class="fas ${
|
|
userRole === "admin"
|
|
? "fa-crown"
|
|
: "fa-user-tie"
|
|
}"></i>
|
|
${
|
|
userRole.charAt(0).toUpperCase() +
|
|
userRole.slice(1)
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-sections">
|
|
<div class="detail-section">
|
|
<h4><i class="fas fa-info-circle"></i> Account Information</h4>
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<strong>Status:</strong>
|
|
<span class="status-badge ${userStatus}">
|
|
<i class="fas ${
|
|
userStatus === "active"
|
|
? "fa-check-circle"
|
|
: "fa-times-circle"
|
|
}"></i>
|
|
${
|
|
userStatus.charAt(0).toUpperCase() +
|
|
userStatus.slice(1)
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<strong>Role:</strong>
|
|
<span>${
|
|
userRole.charAt(0).toUpperCase() +
|
|
userRole.slice(1)
|
|
}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<strong>User ID:</strong>
|
|
<span>${userId}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h4><i class="fas fa-qrcode"></i> QR Code Activity</h4>
|
|
<div class="activity-stats">
|
|
<div class="activity-item">
|
|
<span class="activity-number">${qrCount}</span>
|
|
<span class="activity-label">Total QR Codes</span>
|
|
</div>
|
|
<div class="activity-item">
|
|
<span class="activity-number">${activeQrCount}</span>
|
|
<span class="activity-label">Active QR Codes</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
function closeUserDetailsModal() {
|
|
const modal = document.getElementById("userDetailsModal");
|
|
if (modal) modal.style.display = "none";
|
|
}
|
|
|
|
function resetUserPassword(userId, userName) {
|
|
const modal = document.getElementById("passwordResetModal");
|
|
if (modal) modal.style.display = "flex";
|
|
}
|
|
|
|
function closePasswordResetModal() {
|
|
const modal = document.getElementById("passwordResetModal");
|
|
if (modal) modal.style.display = "none";
|
|
}
|
|
|
|
function viewUserQRCodes(userId) {
|
|
// This would navigate to a page showing the user's QR codes
|
|
alert(
|
|
`This would show QR codes for user ID: ${userId}\nYou can implement this by creating a route like /users/${userId}/qr-codes`
|
|
);
|
|
}
|
|
|
|
// Utility Functions
|
|
function showToast(message, type = "info") {
|
|
const toast = document.createElement("div");
|
|
toast.className = `toast toast-${type}`;
|
|
toast.innerHTML = `
|
|
<div class="toast-content">
|
|
<i class="fas ${getToastIcon(type)}"></i>
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
padding: 1rem;
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
transform: translateX(100%);
|
|
transition: all 0.3s ease;
|
|
border-left: 4px solid ${getToastColor(type)};
|
|
max-width: 400px;
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.opacity = "1";
|
|
toast.style.transform = "translateX(0)";
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
|
toast.style.opacity = "0";
|
|
toast.style.transform = "translateX(100%)";
|
|
setTimeout(() => {
|
|
if (document.body.contains(toast)) {
|
|
document.body.removeChild(toast);
|
|
}
|
|
}, 300);
|
|
}, 4000);
|
|
}
|
|
|
|
function getToastIcon(type) {
|
|
const icons = {
|
|
success: "fa-check-circle",
|
|
error: "fa-exclamation-circle",
|
|
warning: "fa-exclamation-triangle",
|
|
info: "fa-info-circle",
|
|
};
|
|
return icons[type] || icons.info;
|
|
}
|
|
|
|
function getToastColor(type) {
|
|
const colors = {
|
|
success: "#059669",
|
|
error: "#dc2626",
|
|
warning: "#d97706",
|
|
info: "#0891b2",
|
|
};
|
|
return colors[type] || colors.info;
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
/* Users Page Styles */
|
|
.users-page {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
.users-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 2rem;
|
|
padding: 2rem;
|
|
background: var(--white);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.header-content h1 {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.header-content p {
|
|
color: var(--gray-500);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.users-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--white);
|
|
padding: 1.5rem;
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: var(--radius-lg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.25rem;
|
|
color: var(--white);
|
|
}
|
|
|
|
.stat-icon.admin {
|
|
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
|
}
|
|
|
|
.stat-icon.staff {
|
|
background: linear-gradient(135deg, var(--gray-500), var(--gray-600));
|
|
}
|
|
|
|
.stat-icon.active {
|
|
background: linear-gradient(135deg, var(--success-color), #047857);
|
|
}
|
|
|
|
.stat-icon.inactive {
|
|
background: linear-gradient(135deg, var(--danger-color), #b91c1c);
|
|
}
|
|
|
|
.stat-info h3 {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-info p {
|
|
color: var(--gray-500);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.users-controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
padding: 1.5rem;
|
|
background: var(--white);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.search-filters {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.search-box {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.search-box i {
|
|
position: absolute;
|
|
left: 0.75rem;
|
|
color: var(--gray-400);
|
|
z-index: 1;
|
|
}
|
|
|
|
.search-box input {
|
|
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
|
|
border: 2px solid var(--gray-200);
|
|
border-radius: var(--radius-lg);
|
|
font-size: 0.875rem;
|
|
width: 300px;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.search-box input:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.filter-select {
|
|
padding: 0.75rem 1rem;
|
|
border: 2px solid var(--gray-200);
|
|
border-radius: var(--radius-lg);
|
|
font-size: 0.875rem;
|
|
background-color: var(--white);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
min-width: 140px;
|
|
}
|
|
|
|
.filter-select:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.users-container {
|
|
background: var(--white);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.users-table {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.users-table table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
min-width: 1000px;
|
|
}
|
|
|
|
.users-table th,
|
|
.users-table td {
|
|
padding: 1rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.users-table th {
|
|
background-color: var(--gray-50);
|
|
font-weight: 600;
|
|
color: var(--gray-700);
|
|
font-size: 0.875rem;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.users-table tbody tr {
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.users-table tbody tr:hover {
|
|
background-color: var(--gray-50);
|
|
}
|
|
|
|
/* User Information Styles */
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.user-avatar {
|
|
position: relative;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: var(--gray-200);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--gray-500);
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
.avatar-badge {
|
|
position: absolute;
|
|
bottom: -2px;
|
|
right: -2px;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.625rem;
|
|
border: 2px solid var(--white);
|
|
}
|
|
|
|
.avatar-badge.admin {
|
|
background: #fbbf24;
|
|
color: #92400e;
|
|
}
|
|
|
|
.user-details {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 600;
|
|
color: var(--gray-900);
|
|
font-size: 0.875rem;
|
|
margin-bottom: 0.125rem;
|
|
}
|
|
|
|
.user-username {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-500);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.current-user-badge {
|
|
display: inline-block;
|
|
padding: 0.125rem 0.375rem;
|
|
background: rgba(37, 99, 235, 0.1);
|
|
color: var(--primary-color);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.625rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Contact Information */
|
|
.contact-info {
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-email {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.user-email i {
|
|
color: var(--gray-400);
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
/* Role Badge */
|
|
.role-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: var(--radius);
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.role-badge.admin {
|
|
background: rgba(251, 191, 36, 0.1);
|
|
color: #d97706;
|
|
}
|
|
|
|
.role-badge.staff {
|
|
background: rgba(100, 116, 139, 0.1);
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
/* Status Badge */
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: var(--radius);
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-badge.active {
|
|
background: rgba(5, 150, 105, 0.1);
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.status-badge.inactive {
|
|
background: rgba(220, 38, 38, 0.1);
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
/* QR Count */
|
|
.qr-count {
|
|
text-align: center;
|
|
}
|
|
|
|
.qr-total {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.qr-total strong {
|
|
display: block;
|
|
font-size: 1.125rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.qr-total small,
|
|
.qr-active small {
|
|
font-size: 0.625rem;
|
|
color: var(--gray-500);
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.qr-active span {
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: var(--success-color);
|
|
}
|
|
|
|
/* Date Information */
|
|
.date-info {
|
|
text-align: center;
|
|
}
|
|
|
|
.date {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--gray-900);
|
|
margin-bottom: 0.125rem;
|
|
}
|
|
|
|
.time {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
.text-muted {
|
|
color: var(--gray-400);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Action Buttons */
|
|
.user-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
text-decoration: none;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-edit {
|
|
background: rgba(8, 145, 178, 0.1);
|
|
color: var(--info-color);
|
|
}
|
|
|
|
.btn-edit:hover {
|
|
background: rgba(8, 145, 178, 0.2);
|
|
}
|
|
|
|
.btn-promote {
|
|
background: rgba(5, 150, 105, 0.1);
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.btn-promote:hover {
|
|
background: rgba(5, 150, 105, 0.2);
|
|
}
|
|
|
|
.btn-demote {
|
|
background: rgba(217, 119, 6, 0.1);
|
|
color: var(--warning-color);
|
|
}
|
|
|
|
.btn-demote:hover {
|
|
background: rgba(217, 119, 6, 0.2);
|
|
}
|
|
|
|
.btn-deactivate {
|
|
background: rgba(220, 38, 38, 0.1);
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.btn-deactivate:hover {
|
|
background: rgba(220, 38, 38, 0.2);
|
|
}
|
|
|
|
.btn-activate {
|
|
background: rgba(5, 150, 105, 0.1);
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.btn-activate:hover {
|
|
background: rgba(5, 150, 105, 0.2);
|
|
}
|
|
|
|
.btn-more {
|
|
background: rgba(100, 116, 139, 0.1);
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.btn-more:hover {
|
|
background: rgba(100, 116, 139, 0.2);
|
|
}
|
|
|
|
.btn-disabled {
|
|
background: rgba(156, 163, 175, 0.1);
|
|
color: var(--gray-400);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-disabled:hover {
|
|
transform: none;
|
|
}
|
|
|
|
.btn-delete {
|
|
background: rgba(220, 38, 38, 0.1);
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.btn-delete:hover {
|
|
background: rgba(220, 38, 38, 0.2);
|
|
}
|
|
|
|
/* Dropdown Styles */
|
|
.dropdown {
|
|
position: relative;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
background: var(--white);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-xl);
|
|
min-width: 180px;
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(-10px);
|
|
transition: var(--transition);
|
|
padding: 0.5rem 0;
|
|
}
|
|
|
|
.dropdown-menu.show {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.dropdown-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
color: var(--gray-700);
|
|
text-decoration: none;
|
|
font-size: 0.875rem;
|
|
transition: var(--transition);
|
|
border: none;
|
|
background: none;
|
|
width: 100%;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dropdown-item:hover {
|
|
background-color: var(--gray-50);
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.dropdown-item i {
|
|
width: 16px;
|
|
color: var(--gray-400);
|
|
}
|
|
|
|
.dropdown-item.danger-item {
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.dropdown-item.danger-item:hover {
|
|
background-color: rgba(220, 38, 38, 0.1);
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.dropdown-item.danger-item i {
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.dropdown-divider {
|
|
height: 1px;
|
|
background: var(--gray-200);
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
/* Modal Styles */
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 2000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--white);
|
|
border-radius: var(--radius-2xl);
|
|
box-shadow: var(--shadow-xl);
|
|
max-width: 600px;
|
|
width: 100%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
animation: modalSlideUp 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes modalSlideUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px) scale(0.95);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
}
|
|
|
|
.modal-header h3 {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
padding: 0.5rem;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
color: var(--gray-500);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.modal-close:hover {
|
|
background-color: var(--gray-100);
|
|
color: var(--gray-700);
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
padding: 1.5rem;
|
|
border-top: 1px solid var(--gray-200);
|
|
}
|
|
|
|
/* User Detail View */
|
|
.user-detail-view {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.detail-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
background: var(--gray-50);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.detail-avatar {
|
|
position: relative;
|
|
width: 60px;
|
|
height: 60px;
|
|
background: var(--gray-200);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--gray-500);
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.detail-info h3 {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.detail-info p {
|
|
color: var(--gray-500);
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.detail-sections {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.detail-section h4 {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--gray-700);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.detail-grid {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.detail-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.75rem;
|
|
background: var(--white);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.detail-item strong {
|
|
color: var(--gray-700);
|
|
font-weight: 500;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.activity-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.activity-item {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: var(--white);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.activity-number {
|
|
display: block;
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--primary-color);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.activity-label {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-500);
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.loading-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.75rem;
|
|
padding: 2rem;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
.loading-content i {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
/* Delete Warning Modal Styles */
|
|
.delete-warning {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.warning-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
background: rgba(220, 38, 38, 0.1);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--danger-color);
|
|
font-size: 1.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.warning-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.warning-content h4 {
|
|
color: var(--danger-color);
|
|
font-size: 1.125rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.warning-content p {
|
|
color: var(--gray-700);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.warning-details {
|
|
background: rgba(220, 38, 38, 0.05);
|
|
padding: 1rem;
|
|
border-radius: var(--radius);
|
|
border-left: 4px solid var(--danger-color);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.warning-details h5 {
|
|
color: var(--danger-color);
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.warning-details ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.warning-details li {
|
|
padding: 0.25rem 0;
|
|
color: var(--gray-700);
|
|
font-size: 0.875rem;
|
|
position: relative;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.warning-details li::before {
|
|
content: "•";
|
|
position: absolute;
|
|
left: 0;
|
|
color: var(--danger-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.confirmation-section {
|
|
background: var(--gray-50);
|
|
padding: 1rem;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--gray-200);
|
|
}
|
|
|
|
.confirmation-section p {
|
|
margin-bottom: 0.75rem;
|
|
font-weight: 500;
|
|
color: var(--gray-700);
|
|
}
|
|
|
|
.delete-input {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 2px solid var(--gray-300);
|
|
border-radius: var(--radius);
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
background: var(--white);
|
|
}
|
|
|
|
.delete-input:focus {
|
|
outline: none;
|
|
border-color: var(--danger-color);
|
|
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--danger-color);
|
|
color: var(--white);
|
|
border: none;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #b91c1c;
|
|
}
|
|
|
|
.btn-danger:disabled {
|
|
background: var(--gray-400);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-danger-enabled {
|
|
background: var(--danger-color) !important;
|
|
cursor: pointer !important;
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
background: var(--white);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.empty-icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 80px;
|
|
height: 80px;
|
|
background: linear-gradient(135deg, var(--gray-100), var(--gray-200));
|
|
color: var(--gray-400);
|
|
border-radius: 50%;
|
|
font-size: 2rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.empty-state p {
|
|
color: var(--gray-500);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 1200px) {
|
|
.users-page {
|
|
padding: 0 0.5rem;
|
|
}
|
|
|
|
.search-box input {
|
|
width: 250px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.users-header {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.users-stats {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.users-controls {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.search-filters {
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.filter-group {
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.search-box input {
|
|
width: 100%;
|
|
}
|
|
|
|
.filter-select {
|
|
width: 100%;
|
|
}
|
|
|
|
.users-table {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.users-table th,
|
|
.users-table td {
|
|
padding: 0.75rem 0.5rem;
|
|
}
|
|
|
|
.user-actions {
|
|
flex-wrap: wrap;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.action-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.modal-content {
|
|
margin: 0.5rem;
|
|
max-height: calc(100vh - 1rem);
|
|
}
|
|
|
|
.detail-header {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.activity-stats {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.users-stats {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stat-card {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.users-table table {
|
|
min-width: 800px;
|
|
}
|
|
|
|
.user-info {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
/* Accessibility and utility classes */
|
|
.text-center {
|
|
text-align: center;
|
|
}
|
|
.text-left {
|
|
text-align: left;
|
|
}
|
|
.text-right {
|
|
text-align: right;
|
|
}
|
|
|
|
.mb-1 {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.mb-2 {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.mb-3 {
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.mb-4 {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.mt-1 {
|
|
margin-top: 0.25rem;
|
|
}
|
|
.mt-2 {
|
|
margin-top: 0.5rem;
|
|
}
|
|
.mt-3 {
|
|
margin-top: 0.75rem;
|
|
}
|
|
.mt-4 {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.p-1 {
|
|
padding: 0.25rem;
|
|
}
|
|
.p-2 {
|
|
padding: 0.5rem;
|
|
}
|
|
.p-3 {
|
|
padding: 0.75rem;
|
|
}
|
|
.p-4 {
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* Loading states */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid var(--gray-200);
|
|
border-top: 4px solid var(--primary-color);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Success/Error states */
|
|
.success {
|
|
color: var(--success-color);
|
|
}
|
|
.error {
|
|
color: var(--danger-color);
|
|
}
|
|
.warning {
|
|
color: var(--warning-color);
|
|
}
|
|
.info {
|
|
color: var(--info-color);
|
|
}
|
|
|
|
/* Hidden utility */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
.sr-only {
|
|
position: absolute !important;
|
|
width: 1px !important;
|
|
height: 1px !important;
|
|
padding: 0 !important;
|
|
margin: -1px !important;
|
|
overflow: hidden !important;
|
|
clip: rect(0, 0, 0, 0) !important;
|
|
white-space: nowrap !important;
|
|
border: 0 !important;
|
|
}
|
|
|
|
/* Focus states for accessibility */
|
|
.action-btn:focus,
|
|
.dropdown-toggle:focus,
|
|
.btn:focus {
|
|
outline: 2px solid var(--primary-color);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* High contrast mode support */
|
|
@media (prefers-contrast: high) {
|
|
.stat-icon.admin {
|
|
background: #000;
|
|
color: #fff;
|
|
}
|
|
.stat-icon.staff {
|
|
background: #666;
|
|
color: #fff;
|
|
}
|
|
.stat-icon.active {
|
|
background: #0a5a2e;
|
|
color: #fff;
|
|
}
|
|
.stat-icon.inactive {
|
|
background: #a01e1e;
|
|
color: #fff;
|
|
}
|
|
|
|
.role-badge.admin {
|
|
background: #000;
|
|
color: #fff;
|
|
}
|
|
.role-badge.staff {
|
|
background: #666;
|
|
color: #fff;
|
|
}
|
|
|
|
.status-badge.active {
|
|
background: #0a5a2e;
|
|
color: #fff;
|
|
}
|
|
.status-badge.inactive {
|
|
background: #a01e1e;
|
|
color: #fff;
|
|
}
|
|
}
|
|
|
|
/* Reduced motion support */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
|
|
.stat-card:hover,
|
|
.action-btn:hover {
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
/* Print styles */
|
|
@media print {
|
|
.users-controls,
|
|
.header-actions,
|
|
.user-actions,
|
|
.modal,
|
|
.empty-state {
|
|
display: none !important;
|
|
}
|
|
|
|
.users-page {
|
|
max-width: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.users-header {
|
|
box-shadow: none;
|
|
border: 1px solid #000;
|
|
}
|
|
|
|
.users-table {
|
|
box-shadow: none;
|
|
border: 1px solid #000;
|
|
}
|
|
|
|
.users-table th,
|
|
.users-table td {
|
|
border: 1px solid #000;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.stat-card {
|
|
break-inside: avoid;
|
|
box-shadow: none;
|
|
border: 1px solid #000;
|
|
}
|
|
}
|
|
|
|
/* Dark mode support (if implemented) */
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
--white: #1a1a1a;
|
|
--gray-50: #262626;
|
|
--gray-100: #2d2d2d;
|
|
--gray-200: #404040;
|
|
--gray-300: #525252;
|
|
--gray-400: #737373;
|
|
--gray-500: #a3a3a3;
|
|
--gray-600: #d4d4d4;
|
|
--gray-700: #e5e5e5;
|
|
--gray-800: #f5f5f5;
|
|
--gray-900: #ffffff;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|