Initial Codes

This commit is contained in:
NguyenND
2025-07-29 17:02:08 -04:00
committed by GitHub
parent 946b18f142
commit 7459d0fd15
34 changed files with 23480 additions and 0 deletions
+723
View File
@@ -0,0 +1,723 @@
/**
* Attendance Report JavaScript
* Handles filtering, sorting, pagination, and analytics for attendance data
*/
// Global variables
let currentPage = 1;
let entriesPerPage = 50;
let sortColumn = -1;
let sortDirection = 'asc';
let attendanceData = [];
let filteredData = [];
// Charts
let dailyChart = null;
let locationChart = null;
// Initialize page when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Attendance Report page initialized');
initializeReport();
loadAttendanceData();
initializeCharts();
setupEventListeners();
});
function initializeReport() {
// Load data from table
loadTableData();
// Initialize pagination
updatePagination();
// Apply initial filters if any
applyFilters();
}
function loadTableData() {
const table = document.getElementById('attendanceTable');
if (table) {
const rows = table.querySelectorAll('tbody tr');
attendanceData = Array.from(rows).map((row, index) => {
const cells = row.querySelectorAll('td');
return {
id: row.dataset.recordId,
index: index + 1,
employeeId: cells[1] ? cells[1].textContent.trim() : '',
location: cells[2] ? cells[2].textContent.trim() : '',
event: cells[3] ? cells[3].textContent.trim() : '',
date: cells[4] ? cells[4].textContent.trim() : '',
time: cells[5] ? cells[5].textContent.trim() : '',
device: cells[6] ? cells[6].getAttribute('title') || cells[6].textContent.trim() : '',
status: cells[7] ? cells[7].textContent.trim() : '',
element: row
};
});
filteredData = [...attendanceData];
}
}
function setupEventListeners() {
// Entries per page change
const entriesSelect = document.getElementById('entriesPerPage');
if (entriesSelect) {
entriesSelect.addEventListener('change', changeEntriesPerPage);
}
// Filter form
const filtersForm = document.getElementById('filtersForm');
if (filtersForm) {
filtersForm.addEventListener('submit', function(e) {
e.preventDefault();
applyFilters();
});
}
// Real-time employee filter
const employeeFilter = document.getElementById('employee');
if (employeeFilter) {
employeeFilter.addEventListener('input', debounce(applyFilters, 300));
}
// Date and location filters
const dateFilter = document.getElementById('date');
const locationFilter = document.getElementById('location');
if (dateFilter) {
dateFilter.addEventListener('change', applyFilters);
}
if (locationFilter) {
locationFilter.addEventListener('change', applyFilters);
}
}
function changeEntriesPerPage() {
const select = document.getElementById('entriesPerPage');
entriesPerPage = select.value === 'all' ? filteredData.length : parseInt(select.value);
currentPage = 1;
updateTable();
updatePagination();
}
function applyFilters() {
const dateFilter = document.getElementById('date')?.value || '';
const locationFilter = document.getElementById('location')?.value || '';
const employeeFilter = document.getElementById('employee')?.value.toLowerCase() || '';
filteredData = attendanceData.filter(record => {
const matchesDate = !dateFilter || record.date === dateFilter;
const matchesLocation = !locationFilter || record.location === locationFilter;
const matchesEmployee = !employeeFilter ||
record.employeeId.toLowerCase().includes(employeeFilter);
return matchesDate && matchesLocation && matchesEmployee;
});
currentPage = 1;
updateTable();
updatePagination();
updateFilterStats();
}
function clearFilters() {
// Clear form inputs
const form = document.getElementById('filtersForm');
if (form) {
form.reset();
}
// Reset filtered data
filteredData = [...attendanceData];
currentPage = 1;
// Update display
updateTable();
updatePagination();
updateFilterStats();
// Update URL without filters
const url = new URL(window.location);
url.search = '';
window.history.pushState({}, '', url);
}
function sortTable(columnIndex) {
const headers = ['index', 'employeeId', 'location', 'event', 'date', 'time', 'device', 'status'];
const column = headers[columnIndex];
if (sortColumn === columnIndex) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = columnIndex;
sortDirection = 'asc';
}
filteredData.sort((a, b) => {
let aVal = a[column];
let bVal = b[column];
// Handle different data types
if (column === 'date' || column === 'time') {
aVal = new Date(column === 'date' ? aVal : `2000-01-01 ${aVal}`);
bVal = new Date(column === 'date' ? bVal : `2000-01-01 ${bVal}`);
} else if (column === 'index') {
aVal = parseInt(aVal);
bVal = parseInt(bVal);
} else {
aVal = aVal.toString().toLowerCase();
bVal = bVal.toString().toLowerCase();
}
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
updateTable();
updateSortIndicators(columnIndex);
}
function updateSortIndicators(activeColumn) {
const headers = document.querySelectorAll('th[onclick]');
headers.forEach((header, index) => {
const icon = header.querySelector('i');
if (icon) {
if (index === activeColumn) {
icon.className = `fas fa-sort-${sortDirection === 'asc' ? 'up' : 'down'}`;
} else {
icon.className = 'fas fa-sort';
}
}
});
}
function updateTable() {
const tbody = document.querySelector('#attendanceTable tbody');
if (!tbody) return;
// Calculate pagination
const startIndex = (currentPage - 1) * entriesPerPage;
const endIndex = entriesPerPage === filteredData.length ?
filteredData.length :
Math.min(startIndex + entriesPerPage, filteredData.length);
// Hide all rows first
attendanceData.forEach(record => {
if (record.element) {
record.element.style.display = 'none';
}
});
// Show filtered and paginated rows
const visibleData = filteredData.slice(startIndex, endIndex);
visibleData.forEach((record, index) => {
if (record.element) {
record.element.style.display = '';
// Update row number
const firstCell = record.element.querySelector('td:first-child');
if (firstCell) {
firstCell.textContent = startIndex + index + 1;
}
}
});
// Show empty state if no data
showEmptyStateIfNeeded();
}
function showEmptyStateIfNeeded() {
const tbody = document.querySelector('#attendanceTable tbody');
let emptyRow = tbody.querySelector('.empty-row');
if (filteredData.length === 0) {
if (!emptyRow) {
emptyRow = document.createElement('tr');
emptyRow.className = 'empty-row';
emptyRow.innerHTML = `
<td colspan="9" style="text-align: center; padding: 3rem;">
<div style="color: #6b7280;">
<i class="fas fa-search" style="font-size: 2rem; margin-bottom: 1rem; opacity: 0.5;"></i>
<h3>No Records Found</h3>
<p>No attendance records match your current filters.</p>
<button onclick="clearFilters()" class="btn btn-primary" style="margin-top: 1rem;">
<i class="fas fa-refresh"></i> Clear Filters
</button>
</div>
</td>
`;
tbody.appendChild(emptyRow);
}
emptyRow.style.display = '';
} else if (emptyRow) {
emptyRow.style.display = 'none';
}
}
function updatePagination() {
const container = document.getElementById('paginationContainer');
if (!container) return;
const totalPages = Math.ceil(filteredData.length / entriesPerPage);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let paginationHTML = '<div class="pagination">';
// Previous button
paginationHTML += `
<button onclick="goToPage(${currentPage - 1})"
class="pagination-btn"
${currentPage === 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
`;
// Page numbers
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
if (startPage > 1) {
paginationHTML += `<button onclick="goToPage(1)" class="pagination-btn">1</button>`;
if (startPage > 2) {
paginationHTML += '<span class="pagination-ellipsis">...</span>';
}
}
for (let i = startPage; i <= endPage; i++) {
paginationHTML += `
<button onclick="goToPage(${i})"
class="pagination-btn ${i === currentPage ? 'active' : ''}">
${i}
</button>
`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
paginationHTML += '<span class="pagination-ellipsis">...</span>';
}
paginationHTML += `<button onclick="goToPage(${totalPages})" class="pagination-btn">${totalPages}</button>`;
}
// Next button
paginationHTML += `
<button onclick="goToPage(${currentPage + 1})"
class="pagination-btn"
${currentPage === totalPages ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
`;
paginationHTML += '</div>';
// Add pagination info
const startRecord = (currentPage - 1) * entriesPerPage + 1;
const endRecord = Math.min(currentPage * entriesPerPage, filteredData.length);
paginationHTML += `
<div class="pagination-info">
Showing ${startRecord} to ${endRecord} of ${filteredData.length} entries
${filteredData.length !== attendanceData.length ?
`(filtered from ${attendanceData.length} total entries)` : ''}
</div>
`;
container.innerHTML = paginationHTML;
}
function goToPage(page) {
const totalPages = Math.ceil(filteredData.length / entriesPerPage);
if (page < 1 || page > totalPages) return;
currentPage = page;
updateTable();
updatePagination();
// Scroll to top of table
const table = document.getElementById('attendanceTable');
if (table) {
table.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function updateFilterStats() {
// Update stats display if needed
const totalRecords = filteredData.length;
console.log(`Filtered records: ${totalRecords}`);
}
// Record actions
function viewRecordDetails(recordId) {
const record = attendanceData.find(r => r.id == recordId);
if (!record) return;
const modal = document.getElementById('recordModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
if (!modal || !modalTitle || !modalBody) return;
modalTitle.textContent = `Attendance Record - ${record.employeeId}`;
modalBody.innerHTML = `
<div class="record-details">
<div class="detail-grid">
<div class="detail-item">
<strong>Employee ID:</strong>
<span>${record.employeeId}</span>
</div>
<div class="detail-item">
<strong>Location:</strong>
<span>${record.location}</span>
</div>
<div class="detail-item">
<strong>Event:</strong>
<span>${record.event}</span>
</div>
<div class="detail-item">
<strong>Date:</strong>
<span>${record.date}</span>
</div>
<div class="detail-item">
<strong>Time:</strong>
<span>${record.time}</span>
</div>
<div class="detail-item">
<strong>Device:</strong>
<span>${record.device}</span>
</div>
<div class="detail-item">
<strong>Status:</strong>
<span class="status-badge ${record.status.toLowerCase()}">
${record.status}
</span>
</div>
</div>
</div>
`;
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
}
function editRecord(recordId) {
// Placeholder for edit functionality
alert(`Edit functionality for record ${recordId} would be implemented here.`);
}
function deleteRecord(recordId) {
const record = attendanceData.find(r => r.id == recordId);
if (!record) return;
const confirmed = confirm(
`Are you sure you want to delete the attendance record for ${record.employeeId}?\n\n` +
`Date: ${record.date}\n` +
`Time: ${record.time}\n` +
`Location: ${record.location}\n\n` +
'This action cannot be undone.'
);
if (confirmed) {
// Here you would make an API call to delete the record
console.log(`Deleting record ${recordId}`);
// For demo purposes, just remove from current data
const index = attendanceData.findIndex(r => r.id == recordId);
if (index > -1) {
// Remove from DOM
if (attendanceData[index].element) {
attendanceData[index].element.remove();
}
// Remove from data arrays
attendanceData.splice(index, 1);
const filteredIndex = filteredData.findIndex(r => r.id == recordId);
if (filteredIndex > -1) {
filteredData.splice(filteredIndex, 1);
}
// Update display
updateTable();
updatePagination();
showToast('Record deleted successfully', 'success');
}
}
}
function closeRecordModal() {
const modal = document.getElementById('recordModal');
if (modal) {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 200);
}
}
// Export functionality
function exportAttendance() {
const exportData = filteredData.map(record => ({
'Employee ID': record.employeeId,
'Location': record.location,
'Event': record.event,
'Date': record.date,
'Time': record.time,
'Device': record.device,
'Status': record.status
}));
const csv = convertToCSV(exportData);
downloadCSV(csv, `attendance_report_${new Date().toISOString().split('T')[0]}.csv`);
showToast('Attendance data exported successfully', 'success');
}
function convertToCSV(data) {
if (!data.length) return '';
const headers = Object.keys(data[0]);
const csvContent = [
headers.join(','),
...data.map(row =>
headers.map(header => {
const value = row[header];
// Escape commas and quotes
return typeof value === 'string' && (value.includes(',') || value.includes('"'))
? `"${value.replace(/"/g, '""')}"`
: value;
}).join(',')
)
].join('\n');
return csvContent;
}
function downloadCSV(csv, filename) {
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
function refreshReport() {
showToast('Refreshing report...', 'info');
setTimeout(() => {
window.location.reload();
}, 500);
}
// Charts initialization
function initializeCharts() {
loadAttendanceStats();
}
function loadAttendanceStats() {
fetch('/api/attendance/stats')
.then(response => response.json())
.then(data => {
createDailyChart(data.daily_stats || []);
createLocationChart(data.location_stats || []);
})
.catch(error => {
console.error('Error loading attendance stats:', error);
});
}
function createDailyChart(dailyStats) {
const ctx = document.getElementById('dailyChart');
if (!ctx) return;
if (dailyChart) {
dailyChart.destroy();
}
dailyChart = new Chart(ctx, {
type: 'line',
data: {
labels: dailyStats.map(stat => stat.date),
datasets: [{
label: 'Check-ins',
data: dailyStats.map(stat => stat.checkins),
borderColor: '#2563eb',
backgroundColor: 'rgba(37, 99, 235, 0.1)',
tension: 0.4,
fill: true
}, {
label: 'Unique Employees',
data: dailyStats.map(stat => stat.employees),
borderColor: '#059669',
backgroundColor: 'rgba(5, 150, 105, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
function createLocationChart(locationStats) {
const ctx = document.getElementById('locationChart');
if (!ctx) return;
if (locationChart) {
locationChart.destroy();
}
locationChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: locationStats.map(stat => stat.location),
datasets: [{
data: locationStats.map(stat => stat.checkins),
backgroundColor: [
'#2563eb',
'#059669',
'#d97706',
'#dc2626',
'#7c3aed',
'#0891b2',
'#65a30d',
'#c2410c'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
}
}
}
});
}
// Utility functions
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
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);
}, 3000);
}
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;
}
// Global function exports
window.sortTable = sortTable;
window.changeEntriesPerPage = changeEntriesPerPage;
window.clearFilters = clearFilters;
window.goToPage = goToPage;
window.viewRecordDetails = viewRecordDetails;
window.editRecord = editRecord;
window.deleteRecord = deleteRecord;
window.closeRecordModal = closeRecordModal;
window.exportAttendance = exportAttendance;
window.refreshReport = refreshReport;
+457
View File
@@ -0,0 +1,457 @@
class DashboardManager {
constructor() {
this.selectedQRCodes = new Set();
this.allExpanded = false;
this.init();
}
init() {
this.setupEventListeners();
this.addScrollAnimations();
}
animateOut(element, callback) {
element.classList.add("fade-out");
setTimeout(() => {
if (callback) callback();
element.style.display = "none";
}, 300);
}
// Setup event listeners
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
// ESC to close modals
if (e.key === "Escape") {
this.closeQRModal();
}
// Ctrl/Cmd + F to focus search
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
e.preventDefault();
const searchInput = document.getElementById("qrSearch");
if (searchInput) searchInput.focus();
}
// Delete key for bulk delete (when items selected)
if (e.key === "Delete" && this.selectedQRCodes.size > 0) {
e.preventDefault();
this.bulkDeleteQRCodes();
}
});
// Expand/collapse all toggle
const expandToggle = document.getElementById("expandAllToggle");
if (expandToggle) {
expandToggle.addEventListener("click", () => {
this.toggleExpandAll();
});
}
}
// Add scroll animations for QR items
addScrollAnimations() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("animate-in");
}
});
},
{ threshold: 0.1 }
);
const qrItems = document.querySelectorAll(".qr-item");
qrItems.forEach((item) => observer.observe(item));
}
// FIXED: QR Code Toggle Status
toggleQRCodeStatus(qrId) {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
if (!qrItem) return;
const currentStatus = qrItem.dataset.status;
const newStatus = currentStatus === "active" ? "inactive" : "active";
// Show loading state
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
if (toggleBtn && toggleIcon) {
toggleBtn.classList.add("status-loading");
toggleIcon.className = "fas fa-spinner fa-spin";
toggleBtn.disabled = true;
}
// FIXED: Use correct endpoint with POST method for JSON response
fetch(`/qr-codes/${qrId}/toggle-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((result) => {
if (result.success) {
// Update UI with new status
this.updateQRStatus(qrId, result.new_status ? "active" : "inactive");
window.showToast(result.message, "success");
} else {
throw new Error(result.message || "Failed to update status");
}
})
.catch((error) => {
console.error("Status update error:", error);
window.showToast("Failed to update QR code status", "error");
})
.finally(() => {
// Remove loading state
if (toggleBtn && toggleIcon) {
toggleBtn.classList.remove("status-loading");
toggleBtn.disabled = false;
// Restore icon based on current status
const currentStatus = qrItem.dataset.status;
toggleIcon.className = `fas ${
currentStatus === "active" ? "fa-pause" : "fa-play"
}`;
}
});
}
// Update QR status in UI
updateQRStatus(qrId, newStatus) {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
if (!qrItem) return;
// Update data attribute
qrItem.dataset.status = newStatus;
// Update status badge
const statusBadge = qrItem.querySelector(".qr-status");
if (statusBadge) {
statusBadge.className = `qr-status ${newStatus}`;
statusBadge.innerHTML = `
<i class="fas ${
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
}"></i>
${newStatus === "active" ? "Active" : "Inactive"}
`;
}
// Update toggle buttons
this.updateToggleButton(qrId, newStatus);
}
// Update toggle button appearance
updateToggleButton(qrId, status) {
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
const detailToggleBtn = document.getElementById(
`detail-toggle-btn-${qrId}`
);
if (toggleBtn && toggleIcon) {
// Update quick action button
toggleBtn.className = `action-btn btn-status ${
status === "active" ? "btn-deactivate" : "btn-activate"
}`;
toggleBtn.title = `${
status === "active" ? "Deactivate" : "Activate"
} QR Code`;
toggleIcon.className = `fas ${
status === "active" ? "fa-pause" : "fa-play"
}`;
}
if (detailToggleBtn) {
// Update detail action button
detailToggleBtn.className = `btn ${
status === "active" ? "btn-warning" : "btn-success"
}`;
detailToggleBtn.innerHTML = `
<i class="fas ${status === "active" ? "fa-pause" : "fa-play"}"></i>
${status === "active" ? "Deactivate" : "Activate"} QR Code
`;
}
}
async deleteQRCode(qrId, qrName) {
if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return;
try {
// Show loading state
const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`);
if (deleteBtn) {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
}
const response = await fetch(`/qr-codes/${qrId}/delete`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
// Don't try to parse as JSON - just check if request was successful
if (response.ok) {
// Remove QR item from page immediately
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
if (qrItem) {
qrItem.style.transition = "opacity 0.3s";
qrItem.style.opacity = "0";
setTimeout(() => {
qrItem.remove();
if (this.updateResultsCount) this.updateResultsCount();
}, 300);
}
// Use simple alert instead of problematic showToast
alert(`QR code "${qrName}" deleted successfully!`);
} else {
throw new Error(`Server error: ${response.status}`);
}
} catch (error) {
console.error("Delete error:", error);
// Restore button if there was an error
const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`);
if (deleteBtn) {
deleteBtn.disabled = false;
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
}
// Use simple alert instead of problematic showToast
alert("Failed to delete QR code. Please try again.");
}
}
// Show custom delete confirmation dialog
showDeleteConfirmation(qrName) {
return new Promise((resolve) => {
const modal = document.createElement("div");
modal.className = "modal";
modal.style.display = "flex";
modal.innerHTML = `
<div class="modal-content confirmation-modal">
<div class="modal-header">
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Confirm Deletion</h3>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to permanently delete "${qrName}"?</strong></p>
<p class="text-muted">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="confirmDelete()">
<i class="fas fa-trash"></i> Delete
</button>
<button class="btn btn-secondary" onclick="cancelDelete()">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
window.confirmDelete = () => {
document.body.removeChild(modal);
delete window.confirmDelete;
delete window.cancelDelete;
resolve(true);
};
window.cancelDelete = () => {
document.body.removeChild(modal);
delete window.confirmDelete;
delete window.cancelDelete;
resolve(false);
};
// Close on ESC key
const escHandler = (e) => {
if (e.key === "Escape") {
window.cancelDelete();
document.removeEventListener("keydown", escHandler);
}
};
document.addEventListener("keydown", escHandler);
// Close on backdrop click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
window.cancelDelete();
}
});
});
}
// Bulk delete functionality
async bulkDeleteQRCodes() {
if (this.selectedQRCodes.size === 0) return;
const confirmed = await this.showBulkDeleteConfirmation(this.selectedQRCodes.size);
if (!confirmed) return;
const deletePromises = Array.from(this.selectedQRCodes).map(qrId => {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
const qrName = qrItem ? qrItem.querySelector('.qr-name')?.textContent || 'Unknown' : 'Unknown';
return this.deleteQRCode(qrId, qrName);
});
try {
await Promise.all(deletePromises);
this.selectedQRCodes.clear();
window.showToast(`Successfully deleted ${deletePromises.length} QR codes`, "success");
} catch (error) {
console.error("Bulk delete error:", error);
window.showToast("Some QR codes could not be deleted", "error");
}
}
showBulkDeleteConfirmation(count) {
return new Promise((resolve) => {
const modal = document.createElement("div");
modal.className = "modal";
modal.style.display = "flex";
modal.innerHTML = `
<div class="modal-content confirmation-modal">
<div class="modal-header">
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Confirm Bulk Deletion</h3>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to permanently delete ${count} QR codes?</strong></p>
<p class="text-muted">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="confirmBulkDelete()">
<i class="fas fa-trash"></i> Delete All
</button>
<button class="btn btn-secondary" onclick="cancelBulkDelete()">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
window.confirmBulkDelete = () => {
document.body.removeChild(modal);
delete window.confirmBulkDelete;
delete window.cancelBulkDelete;
resolve(true);
};
window.cancelBulkDelete = () => {
document.body.removeChild(modal);
delete window.confirmBulkDelete;
delete window.cancelBulkDelete;
resolve(false);
};
});
}
// QR Modal functions
openQRModal(qrData) {
const modal = document.getElementById("qrModal");
const modalImage = document.getElementById("modalQRImage");
const modalTitle = document.getElementById("modalTitle");
if (modal && modalImage && modalTitle) {
modalTitle.textContent = `QR Code: ${qrData.name}`;
modalImage.src = qrData.image;
modal.style.display = "flex";
}
}
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.style.display = "none";
}
}
// Expand/collapse functionality
toggleExpandAll() {
const qrItems = document.querySelectorAll(".qr-item");
const expandToggle = document.getElementById("expandAllToggle");
this.allExpanded = !this.allExpanded;
qrItems.forEach((item) => {
if (this.allExpanded) {
item.classList.add("expanded");
} else {
item.classList.remove("expanded");
}
});
if (expandToggle) {
expandToggle.innerHTML = this.allExpanded
? '<i class="fas fa-compress-alt"></i> Collapse All'
: '<i class="fas fa-expand-alt"></i> Expand All';
}
}
toggleQRItem(element) {
element.classList.toggle("expanded");
}
// Copy QR data to clipboard
copyQRData(name, location, address, event) {
const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`;
navigator.clipboard
.writeText(data)
.then(() => {
window.showToast("QR code information copied to clipboard!", "success");
})
.catch(() => {
window.showToast("Failed to copy to clipboard", "error");
});
}
// Update results display
updateResultsDisplay(count) {
const resultsDisplay = document.getElementById("resultsDisplay");
if (resultsDisplay) {
resultsDisplay.textContent = `${count} QR codes found`;
}
}
updateResultsCount() {
const qrItems = document.querySelectorAll(
'.qr-item[style*="block"], .qr-item:not([style*="none"])'
);
const counter = document.querySelector(".results-counter");
if (counter) {
counter.textContent = `${qrItems.length} results`;
}
}
}
// Initialize dashboard when DOM is loaded
document.addEventListener("DOMContentLoaded", function () {
window.dashboardManager = new DashboardManager();
// Global functions for inline event handlers
window.toggleQRCodeStatus = (qrId) =>
window.dashboardManager.toggleQRCodeStatus(qrId);
window.deleteQRCode = (qrId, qrName) =>
window.dashboardManager.deleteQRCode(qrId, qrName);
window.openQRModal = (qrData) => window.dashboardManager.openQRModal(qrData);
window.closeQRModal = () => window.dashboardManager.closeQRModal();
window.toggleQRItem = (element) =>
window.dashboardManager.toggleQRItem(element);
window.copyQRData = (name, location, address, event) =>
window.dashboardManager.copyQRData(name, location, address, event);
});
+573
View File
@@ -0,0 +1,573 @@
/**
* Dashboard-specific JavaScript functionality with QR Toggle
* static/js/dashboard.js
*/
// Dashboard QR Management Class
class DashboardManager {
constructor() {
this.allExpanded = false;
this.currentModalQR = null;
this.init();
}
init() {
this.initializeSearch();
this.initializeFilters();
this.setupEventListeners();
this.addScrollAnimations();
this.updateResultsCount();
}
// Initialize search functionality with debouncing
initializeSearch() {
const searchInput = document.getElementById("qrSearch");
if (!searchInput) return;
let searchTimeout;
searchInput.addEventListener("input", () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.filterQRCodes();
}, 300);
});
}
// Initialize filter functionality
initializeFilters() {
const statusFilter = document.getElementById("statusFilter");
if (!statusFilter) return;
statusFilter.addEventListener("change", () => {
this.filterQRCodes();
});
}
// Enhanced QR code filtering with animation
filterQRCodes() {
const searchTerm = document.getElementById("qrSearch").value.toLowerCase();
const statusFilter = document.getElementById("statusFilter").value;
const qrItems = document.querySelectorAll(".qr-item");
let visibleCount = 0;
qrItems.forEach((item) => {
const name = item.dataset.name || "";
const location = item.dataset.location || "";
const status = item.dataset.status || "";
const matchesSearch =
!searchTerm ||
name.includes(searchTerm) ||
location.includes(searchTerm);
const matchesStatus = !statusFilter || status === statusFilter;
if (matchesSearch && matchesStatus) {
this.showQRItem(item);
visibleCount++;
} else {
this.hideQRItem(item);
}
});
this.updateResultsDisplay(visibleCount);
this.updateResultsCount();
}
// Show QR item with animation
showQRItem(item) {
item.style.display = "block";
setTimeout(() => {
item.classList.add("fade-in");
item.classList.remove("fade-out");
}, 10);
}
// Hide QR item with animation
hideQRItem(item) {
item.classList.add("fade-out");
item.classList.remove("fade-in");
setTimeout(() => {
item.style.display = "none";
}, 300);
}
// Update results display and empty state
updateResultsDisplay(count) {
const qrList = document.getElementById("qrList");
let existingEmpty = document.querySelector(".search-empty-state");
if (
count === 0 &&
(document.getElementById("qrSearch").value ||
document.getElementById("statusFilter").value)
) {
if (!existingEmpty) {
const emptyState = document.createElement("div");
emptyState.className = "search-empty-state";
emptyState.innerHTML = `
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3>No QR Codes Found</h3>
<p>Try adjusting your search or filter criteria</p>
<button onclick="dashboardManager.clearFilters()" class="btn btn-outline">
<i class="fas fa-refresh"></i>
Clear Filters
</button>
`;
qrList.parentNode.appendChild(emptyState);
}
} else {
if (existingEmpty) {
existingEmpty.remove();
}
}
}
// Clear all filters
clearFilters() {
document.getElementById("qrSearch").value = "";
document.getElementById("statusFilter").value = "";
this.filterQRCodes();
}
// Update results counter
updateResultsCount() {
const qrItems = document.querySelectorAll(
'.qr-item[style*="block"], .qr-item:not([style*="none"])'
);
const totalItems = document.querySelectorAll(".qr-item").length;
const visibleCount = qrItems.length;
let counter = document.querySelector(".results-counter");
if (!counter) {
counter = document.createElement("div");
counter.className = "results-counter";
const searchContainer = document.querySelector(".search-container");
if (searchContainer) {
searchContainer.appendChild(counter);
}
}
if (visibleCount !== totalItems) {
counter.textContent = `Showing ${visibleCount} of ${totalItems} QR codes`;
counter.style.display = "block";
} else {
counter.style.display = "none";
}
}
// Setup additional event listeners
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case "f":
e.preventDefault();
document.getElementById("qrSearch")?.focus();
break;
}
}
});
// Enhanced modal functionality
this.setupModalHandling();
}
// Enhanced modal handling
setupModalHandling() {
const modal = document.getElementById("qrModal");
if (!modal) return;
// Close modal with better animation
const closeButtons = modal.querySelectorAll("[onclick*='closeQRModal']");
closeButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
this.closeQRModal();
});
});
// Click outside to close
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeQRModal();
}
});
// Escape key to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.style.display === "flex") {
this.closeQRModal();
}
});
}
// Close QR modal with improved animation
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.classList.remove("show");
setTimeout(() => {
modal.style.display = "none";
}, 200);
}
this.currentModalQR = null;
}
// Add scroll animations for better UX
addScrollAnimations() {
const observerOptions = {
threshold: 0.1,
rootMargin: "0px 0px -50px 0px",
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.opacity = "1";
entry.target.style.transform = "translateY(0)";
}
});
}, observerOptions);
// Observe QR items
document.querySelectorAll(".qr-item").forEach((item) => {
item.style.opacity = "0";
item.style.transform = "translateY(20px)";
item.style.transition = "opacity 0.6s ease, transform 0.6s ease";
observer.observe(item);
});
}
// Toggle QR details with smooth animation
toggleQRDetails(qrId) {
const details = document.getElementById(`qr-details-${qrId}`);
const chevron = document.querySelector(
`[onclick="toggleQRDetails(${qrId})"] .chevron`
);
if (!details) return;
if (details.classList.contains("expanded")) {
details.classList.remove("expanded");
if (chevron) chevron.style.transform = "rotate(0deg)";
} else {
// Close other expanded items first
document
.querySelectorAll(".qr-item-details.expanded")
.forEach((detail) => {
if (detail !== details) {
detail.classList.remove("expanded");
}
});
details.classList.add("expanded");
if (chevron) chevron.style.transform = "rotate(180deg)";
}
}
// Toggle all QR codes expand/collapse
toggleAllQRs() {
const details = document.querySelectorAll(".qr-item-details");
const expandIcon = document.getElementById("expandIcon");
const expandText = document.getElementById("expandText");
this.allExpanded = !this.allExpanded;
details.forEach((detail) => {
if (this.allExpanded) {
detail.classList.add("expanded");
} else {
detail.classList.remove("expanded");
}
});
// Update button text and icon
if (expandIcon && expandText) {
if (this.allExpanded) {
expandIcon.className = "fas fa-compress-alt";
expandText.textContent = "Collapse All";
} else {
expandIcon.className = "fas fa-expand-alt";
expandText.textContent = "Expand All";
}
}
// Update chevron icons
document.querySelectorAll(".chevron").forEach((chevron) => {
chevron.style.transform = this.allExpanded
? "rotate(180deg)"
: "rotate(0deg)";
});
}
// Enhanced QR preview functionality
previewQR(qrData, qrName) {
const modal = document.getElementById("qrModal");
const modalTitle = document.getElementById("modalTitle");
const modalImage = document.getElementById("modalQRImage");
if (modal && modalTitle && modalImage) {
modalTitle.textContent = `${qrName} - QR Code`;
modalImage.src = `data:image/png;base64,${qrData}`;
modalImage.alt = `QR Code for ${qrName}`;
this.currentModalQR = {
name: qrName,
image: `data:image/png;base64,${qrData}`,
};
modal.style.display = "flex";
setTimeout(() => modal.classList.add("show"), 10);
}
}
// Enhanced download functionality
downloadQR(base64Image, filename) {
try {
const link = document.createElement("a");
link.href = `data:image/png;base64,${base64Image}`;
link.download = `${filename
.replace(/[^a-z0-9]/gi, "_")
.toLowerCase()}_qr_code.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success toast
this.showToast("QR code downloaded successfully!", "success");
} catch (error) {
this.showToast("Failed to download QR code", "error");
console.error("Download error:", error);
}
}
// Download from modal
downloadModalQR() {
if (this.currentModalQR) {
const base64Data = this.currentModalQR.image.split("base64,")[1];
this.downloadQR(base64Data, this.currentModalQR.name);
}
}
// NEW: Toggle QR Code Status (Activate/Deactivate)
async toggleQRCodeStatus(qrId) {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
const statusElement = document.getElementById(`status-${qrId}`);
const statusIcon = document.getElementById(`status-icon-${qrId}`);
const statusText = document.getElementById(`status-text-${qrId}`);
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
const detailToggleBtn = document.getElementById(
`detail-toggle-btn-${qrId}`
);
if (!statusElement || !qrItem) return;
// Add loading state
statusElement.classList.add("status-loading");
if (toggleBtn) toggleBtn.disabled = true;
if (detailToggleBtn) detailToggleBtn.disabled = true;
try {
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success) {
// Update UI elements
const newStatus = data.new_status;
const newStatusClass = newStatus ? "active" : "inactive";
const newIconClass = newStatus ? "fa-check-circle" : "fa-times-circle";
const newToggleIcon = newStatus ? "fa-pause" : "fa-play";
const newToggleBtnClass = newStatus ? "btn-deactivate" : "btn-activate";
const newDetailBtnClass = newStatus ? "btn-warning" : "btn-success";
const newDetailBtnText = newStatus ? "Deactivate" : "Activate";
// Update status badge
statusElement.className = `qr-status ${newStatusClass}`;
if (statusIcon) statusIcon.className = `fas ${newIconClass}`;
if (statusText) statusText.textContent = data.status_text;
// Update QR item data attribute and styling
qrItem.setAttribute("data-status", newStatusClass);
// Update toggle button (collapsed view)
if (toggleBtn) {
toggleBtn.className = `action-btn btn-status ${newToggleBtnClass}`;
toggleBtn.title = `${newDetailBtnText} QR Code`;
}
if (toggleIcon) {
toggleIcon.className = `fas ${newToggleIcon}`;
}
// Update detail toggle button (expanded view)
if (detailToggleBtn) {
detailToggleBtn.className = `btn ${newDetailBtnClass}`;
detailToggleBtn.innerHTML = `<i class="fas ${newToggleIcon}"></i> ${newDetailBtnText} QR Code`;
}
// Update status badges in expanded view
const detailStatusBadges = qrItem.querySelectorAll(".status-badge");
detailStatusBadges.forEach((badge) => {
badge.className = `status-badge ${newStatusClass}`;
const icon = badge.querySelector("i");
if (icon) icon.className = `fas ${newIconClass}`;
const text = badge.textContent.trim();
if (text === "Active" || text === "Inactive") {
badge.innerHTML = `<i class="fas ${newIconClass}"></i> ${data.status_text}`;
}
});
// Show success message
this.showToast(data.message, "success");
// Update statistics if needed
this.updateStatistics();
} else {
this.showToast(
data.message || "Failed to update QR code status",
"error"
);
}
} catch (error) {
console.error("Error toggling QR status:", error);
this.showToast("Network error. Please try again.", "error");
} finally {
// Remove loading state
statusElement.classList.remove("status-loading");
if (toggleBtn) toggleBtn.disabled = false;
if (detailToggleBtn) detailToggleBtn.disabled = false;
}
}
// Update statistics after status change
updateStatistics() {
const activeCount = document.querySelectorAll(
'[data-status="active"]'
).length;
const inactiveCount = document.querySelectorAll(
'[data-status="inactive"]'
).length;
const totalCount = activeCount + inactiveCount;
// Update active count
const activeStatElement = document.querySelector(".stat-card.success h3");
if (activeStatElement) {
activeStatElement.textContent = activeCount;
}
// Update active percentage
const activePercentElement = document.querySelector(
".stat-card.success .stat-trend"
);
if (activePercentElement && totalCount > 0) {
const percentage = ((activeCount / totalCount) * 100).toFixed(1);
activePercentElement.textContent = `${percentage}% active`;
}
// Update inactive count if there's a specific stat card for it
const inactiveStatElement = document.querySelector(".stat-card.warning h3");
if (inactiveStatElement) {
inactiveStatElement.textContent = inactiveCount;
}
// Update inactive percentage
const inactivePercentElement = document.querySelector(
".stat-card.warning .stat-trend"
);
if (inactivePercentElement && totalCount > 0) {
const percentage = ((inactiveCount / totalCount) * 100).toFixed(1);
inactivePercentElement.textContent = `${percentage}% inactive`;
}
}
// Toast notification system
showToast(message, type = "info") {
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-content">
<i class="fas ${this.getToastIcon(type)}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(toast);
// Animate in
setTimeout(() => toast.classList.add("show"), 100);
// Auto remove
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 3000);
}
// Get toast icon based on type
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;
}
}
// Global functions for compatibility with existing onclick handlers
let dashboardManager;
function toggleQRDetails(qrId) {
dashboardManager?.toggleQRDetails(qrId);
}
function toggleAllQRs() {
dashboardManager?.toggleAllQRs();
}
function previewQR(qrData, qrName) {
dashboardManager?.previewQR(qrData, qrName);
}
function downloadQR(base64Image, filename) {
dashboardManager?.downloadQR(base64Image, filename);
}
function closeQRModal() {
dashboardManager?.closeQRModal();
}
function downloadModalQR() {
dashboardManager?.downloadModalQR();
}
// NEW: Global function for QR status toggle
function toggleQRCodeStatus(qrId) {
dashboardManager?.toggleQRCodeStatus(qrId);
}
// Initialize dashboard when DOM is ready
document.addEventListener("DOMContentLoaded", function () {
dashboardManager = new DashboardManager();
// Add helpful keyboard shortcuts tooltip
console.log("Dashboard keyboard shortcuts:");
console.log("Ctrl/Cmd + F: Focus search");
console.log("Escape: Close modal");
});
+590
View File
@@ -0,0 +1,590 @@
/**
* QR Code Destination Page JavaScript
* Handles staff check-in functionality and form interactions
*/
// Global variables
let isSubmitting = false;
let currentTime = new Date();
// Initialize page when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('QR Destination page initialized');
initializePage();
setupEventListeners();
startTimeUpdater();
});
function initializePage() {
// Focus on employee ID input
const employeeInput = document.getElementById('employee_id');
if (employeeInput) {
employeeInput.focus();
}
// Initialize current time display
updateCurrentTime();
// Add page load animation
document.body.classList.add('page-loaded');
}
function setupEventListeners() {
const form = document.getElementById('checkinForm');
const employeeInput = document.getElementById('employee_id');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}
if (employeeInput) {
// Real-time input validation
employeeInput.addEventListener('input', handleInputChange);
employeeInput.addEventListener('blur', validateEmployeeId);
employeeInput.addEventListener('keypress', handleKeyPress);
// Auto-uppercase input
employeeInput.addEventListener('input', function() {
this.value = this.value.toUpperCase();
});
}
// Handle page visibility changes
document.addEventListener('visibilitychange', handleVisibilityChange);
}
function handleFormSubmit(e) {
e.preventDefault();
if (isSubmitting) {
return false;
}
const employeeId = document.getElementById('employee_id').value.trim();
if (!validateEmployeeId()) {
return false;
}
submitCheckin(employeeId);
}
function handleInputChange(e) {
const input = e.target;
const value = input.value.trim();
// Clear previous validation states
input.classList.remove('error', 'success');
hideStatusMessage();
// Real-time validation feedback
if (value.length >= 3) {
if (isValidEmployeeId(value)) {
input.classList.add('success');
} else {
input.classList.add('error');
}
}
}
function handleKeyPress(e) {
// Allow only alphanumeric characters
const char = String.fromCharCode(e.which);
if (!/[A-Za-z0-9]/.test(char)) {
e.preventDefault();
shakeInput(e.target);
}
// Submit on Enter key
if (e.key === 'Enter') {
e.preventDefault();
handleFormSubmit(e);
}
}
function validateEmployeeId() {
const employeeInput = document.getElementById('employee_id');
const employeeId = employeeInput.value.trim();
if (!employeeId) {
showValidationError(employeeInput, 'Employee ID is required');
return false;
}
if (employeeId.length < 3) {
showValidationError(employeeInput, 'Employee ID must be at least 3 characters');
return false;
}
if (employeeId.length > 20) {
showValidationError(employeeInput, 'Employee ID must be less than 20 characters');
return false;
}
if (!isValidEmployeeId(employeeId)) {
showValidationError(employeeInput, 'Employee ID can only contain letters and numbers');
return false;
}
// Clear validation error
employeeInput.classList.remove('error');
employeeInput.classList.add('success');
hideStatusMessage();
return true;
}
function isValidEmployeeId(id) {
return /^[A-Za-z0-9]{3,20}$/.test(id);
}
function showValidationError(input, message) {
input.classList.add('error');
input.classList.remove('success');
showStatusMessage(message, 'error');
shakeInput(input);
input.focus();
}
function shakeInput(input) {
input.classList.add('shake');
setTimeout(() => {
input.classList.remove('shake');
}, 500);
}
function submitCheckin(employeeId) {
if (isSubmitting) return;
isSubmitting = true;
showLoadingState();
showLoadingOverlay();
// Prepare form data
const formData = new FormData();
formData.append('employee_id', employeeId);
// Submit to server
fetch(`/qr/${window.qrUrl}/checkin`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
handleCheckinResponse(data);
})
.catch(error => {
console.error('Check-in error:', error);
handleCheckinError('Network error. Please check your connection and try again.');
})
.finally(() => {
isSubmitting = false;
hideLoadingState();
hideLoadingOverlay();
});
}
function handleCheckinResponse(data) {
if (data.success) {
showSuccessCard(data.data);
logSuccessfulCheckin(data.data);
// Optional: Analytics tracking
if (typeof gtag !== 'undefined') {
gtag('event', 'checkin_success', {
'location': window.locationName,
'event_name': window.eventName
});
}
} else {
handleCheckinError(data.message);
}
}
function handleCheckinError(message) {
showStatusMessage(message, 'error');
// Shake the form to draw attention
const form = document.getElementById('checkinForm');
if (form) {
form.classList.add('shake');
setTimeout(() => {
form.classList.remove('shake');
}, 500);
}
// Re-focus on input
const employeeInput = document.getElementById('employee_id');
if (employeeInput) {
employeeInput.focus();
employeeInput.select();
}
}
function showSuccessCard(data) {
// Hide the check-in form
const checkinCard = document.querySelector('.checkin-card');
if (checkinCard) {
checkinCard.style.display = 'none';
}
// Populate and show success card
const successCard = document.getElementById('successCard');
if (successCard) {
document.getElementById('successEmployeeId').textContent = data.employee_id || '-';
document.getElementById('successLocation').textContent = data.location || '-';
document.getElementById('successEvent').textContent = data.event || '-';
document.getElementById('successTime').textContent = data.time || '-';
document.getElementById('successDate').textContent = data.date || '-';
successCard.style.display = 'block';
successCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Optional: Auto-hide success card after some time
setTimeout(() => {
showAutoHideOption();
}, 10000); // 10 seconds
}
function showAutoHideOption() {
const successCard = document.getElementById('successCard');
if (successCard && successCard.style.display !== 'none') {
const actions = successCard.querySelector('.success-actions');
if (actions && !actions.querySelector('.auto-hide-btn')) {
const autoHideBtn = document.createElement('button');
autoHideBtn.className = 'btn btn-outline auto-hide-btn';
autoHideBtn.innerHTML = '<i class="fas fa-clock"></i> Auto-hide in <span id="countdown">30</span>s';
actions.appendChild(autoHideBtn);
startCountdown(30, () => {
checkInAnother();
});
}
}
}
function startCountdown(seconds, callback) {
const countdownElement = document.getElementById('countdown');
let remaining = seconds;
const interval = setInterval(() => {
remaining--;
if (countdownElement) {
countdownElement.textContent = remaining;
}
if (remaining <= 0) {
clearInterval(interval);
callback();
}
}, 1000);
}
function checkInAnother() {
// Show the check-in form again
const checkinCard = document.querySelector('.checkin-card');
const successCard = document.getElementById('successCard');
if (checkinCard) {
checkinCard.style.display = 'block';
}
if (successCard) {
successCard.style.display = 'none';
}
// Reset form
const form = document.getElementById('checkinForm');
if (form) {
form.reset();
}
// Clear validation states
const employeeInput = document.getElementById('employee_id');
if (employeeInput) {
employeeInput.classList.remove('error', 'success');
employeeInput.focus();
}
hideStatusMessage();
// Scroll back to form
checkinCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function showLoadingState() {
const btn = document.querySelector('.btn-primary');
if (btn) {
const content = btn.querySelector('.btn-content');
const loader = btn.querySelector('.btn-loader');
if (content) content.style.display = 'none';
if (loader) loader.style.display = 'flex';
btn.disabled = true;
}
}
function hideLoadingState() {
const btn = document.querySelector('.btn-primary');
if (btn) {
const content = btn.querySelector('.btn-content');
const loader = btn.querySelector('.btn-loader');
if (content) content.style.display = 'flex';
if (loader) loader.style.display = 'none';
btn.disabled = false;
}
}
function showLoadingOverlay() {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = 'flex';
setTimeout(() => {
overlay.classList.add('show');
}, 10);
}
}
function hideLoadingOverlay() {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.classList.remove('show');
setTimeout(() => {
overlay.style.display = 'none';
}, 200);
}
}
function showStatusMessage(message, type = 'info') {
const statusDiv = document.getElementById('statusMessage');
if (statusDiv) {
statusDiv.textContent = message;
statusDiv.className = `status-message ${type}`;
statusDiv.style.display = 'block';
// Auto-hide success messages
if (type === 'success') {
setTimeout(() => {
hideStatusMessage();
}, 5000);
}
// Scroll to message
statusDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function hideStatusMessage() {
const statusDiv = document.getElementById('statusMessage');
if (statusDiv) {
statusDiv.style.display = 'none';
}
}
function updateCurrentTime() {
const timeElement = document.getElementById('currentTime');
if (timeElement) {
const now = new Date();
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
timeElement.textContent = now.toLocaleDateString('en-US', options);
}
}
function startTimeUpdater() {
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
}
function handleVisibilityChange() {
if (document.hidden) {
// Page is hidden - pause operations
console.log('Page hidden - pausing operations');
} else {
// Page is visible - resume operations
console.log('Page visible - resuming operations');
updateCurrentTime();
// Re-focus on input if form is visible
const checkinCard = document.querySelector('.checkin-card');
const employeeInput = document.getElementById('employee_id');
if (checkinCard && checkinCard.style.display !== 'none' && employeeInput) {
setTimeout(() => {
employeeInput.focus();
}, 100);
}
}
}
function logSuccessfulCheckin(data) {
console.log('Successful check-in:', {
employee_id: data.employee_id,
location: data.location,
event: data.event,
time: data.time,
date: data.date
});
}
// Utility functions
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}
// Export functions for global access
window.checkInAnother = checkInAnother;
window.validateEmployeeId = validateEmployeeId;
// Service Worker registration for offline support (optional)
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('ServiceWorker registration successful');
})
.catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
// Error handling for unhandled promises
window.addEventListener('unhandledrejection', function(event) {
console.error('Unhandled promise rejection:', event.reason);
handleCheckinError('An unexpected error occurred. Please try again.');
event.preventDefault();
});
// Handle online/offline status
window.addEventListener('online', function() {
showStatusMessage('Connection restored', 'success');
});
window.addEventListener('offline', function() {
showStatusMessage('No internet connection. Please check your network.', 'warning');
});
// Performance monitoring
if ('performance' in window) {
window.addEventListener('load', function() {
setTimeout(function() {
const perfData = performance.getEntriesByType('navigation')[0];
console.log('Page load time:', perfData.loadEventEnd - perfData.loadEventStart, 'ms');
}, 0);
});
}
// Accessibility enhancements
document.addEventListener('keydown', function(e) {
// Escape key to reset form
if (e.key === 'Escape') {
const successCard = document.getElementById('successCard');
if (successCard && successCard.style.display !== 'none') {
checkInAnother();
} else {
// Reset form
const form = document.getElementById('checkinForm');
if (form) {
form.reset();
hideStatusMessage();
const employeeInput = document.getElementById('employee_id');
if (employeeInput) {
employeeInput.classList.remove('error', 'success');
employeeInput.focus();
}
}
}
}
// Ctrl+R to refresh (prevent default and reload page cleanly)
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
e.preventDefault();
window.location.reload();
}
});
// Touch device optimizations
if ('ontouchstart' in window) {
// Add touch-friendly classes
document.body.classList.add('touch-device');
// Prevent zoom on input focus for iOS
const inputs = document.querySelectorAll('input[type="text"]');
inputs.forEach(input => {
input.addEventListener('focus', function() {
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
}
});
input.addEventListener('blur', function() {
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0');
}
});
});
}
// Auto-refresh page if idle for too long (optional)
let idleTimer;
const IDLE_TIME = 30 * 60 * 1000; // 30 minutes
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (confirm('This page has been idle for 30 minutes. Would you like to refresh it?')) {
window.location.reload();
} else {
resetIdleTimer(); // Reset timer if user chooses not to refresh
}
}, IDLE_TIME);
}
// Track user activity
['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => {
document.addEventListener(event, resetIdleTimer, true);
});
// Initialize idle timer
resetIdleTimer();
+487
View File
@@ -0,0 +1,487 @@
/**
* Main JavaScript functionality for QR Code Management System
* static/js/script.js
*/
// Global configuration
const QRManager = {
config: {
modalCloseDelay: 300,
searchDelay: 300,
animationDuration: 300,
toastDuration: 5000,
},
// Utility functions
utils: {
// Show toast notification
showToast(message, type = "info") {
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-content">
<i class="fas ${this.getToastIcon(type)}"></i>
<span>${message}</span>
<button onclick="this.parentElement.parentElement.remove()" class="toast-close">
<i class="fas fa-times"></i>
</button>
</div>
`;
document.body.appendChild(toast);
// Auto remove after delay
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, QRManager.config.toastDuration);
},
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;
},
// Debounce function
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// Format date
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
},
// Format time
formatTime(dateString) {
const date = new Date(dateString);
return date.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
});
},
},
};
// Navigation functionality
class NavigationManager {
constructor() {
this.initMobileMenu();
this.initDropdowns();
}
initMobileMenu() {
const mobileMenuBtn = document.getElementById("mobile-menu");
const navMenu = document.getElementById("navMenu");
if (mobileMenuBtn && navMenu) {
mobileMenuBtn.addEventListener("click", () => {
mobileMenuBtn.classList.toggle("active");
navMenu.classList.toggle("active");
});
// Close menu when clicking nav links
const navLinks = navMenu.querySelectorAll(".nav-link");
navLinks.forEach((link) => {
link.addEventListener("click", () => {
mobileMenuBtn.classList.remove("active");
navMenu.classList.remove("active");
});
});
}
}
initDropdowns() {
const dropdowns = document.querySelectorAll(".dropdown");
dropdowns.forEach((dropdown) => {
const trigger = dropdown.querySelector(".dropdown-trigger");
const menu = dropdown.querySelector(".dropdown-menu");
if (trigger && menu) {
trigger.addEventListener("click", (e) => {
e.stopPropagation();
this.toggleDropdown(dropdown);
});
}
});
// Close dropdowns when clicking outside
document.addEventListener("click", () => {
this.closeAllDropdowns();
});
}
toggleDropdown(dropdown) {
const menu = dropdown.querySelector(".dropdown-menu");
const isOpen = menu.classList.contains("show");
this.closeAllDropdowns();
if (!isOpen) {
menu.classList.add("show");
}
}
closeAllDropdowns() {
const openMenus = document.querySelectorAll(".dropdown-menu.show");
openMenus.forEach((menu) => {
menu.classList.remove("show");
});
}
}
// Modal management
class ModalManager {
constructor() {
this.initModals();
}
initModals() {
const modals = document.querySelectorAll(".modal");
modals.forEach((modal) => {
// Close button functionality
const closeBtn = modal.querySelector(".modal-close");
if (closeBtn) {
closeBtn.addEventListener("click", () => this.closeModal(modal));
}
// Click outside to close
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeModal(modal);
}
});
});
// Escape key to close all modals
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
this.closeAllModals();
}
});
}
closeModal(modal) {
modal.classList.remove("show");
setTimeout(() => {
modal.style.display = "none";
}, QRManager.config.modalCloseDelay);
}
closeAllModals() {
const openModals = document.querySelectorAll('.modal[style*="flex"]');
openModals.forEach((modal) => this.closeModal(modal));
}
openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = "flex";
setTimeout(() => {
modal.classList.add("show");
}, 10);
}
}
}
// Search functionality
class SearchManager {
constructor(searchInputId, resultsContainerId) {
this.searchInput = document.getElementById(searchInputId);
this.resultsContainer = document.getElementById(resultsContainerId);
this.originalItems = [];
if (this.searchInput && this.resultsContainer) {
this.init();
}
}
init() {
// Store original items
this.originalItems = Array.from(this.resultsContainer.children);
// Add search event listener with debouncing
this.searchInput.addEventListener(
"input",
QRManager.utils.debounce(
() => this.performSearch(),
QRManager.config.searchDelay
)
);
}
performSearch() {
const searchTerm = this.searchInput.value.toLowerCase().trim();
this.originalItems.forEach((item) => {
const searchableText = this.getSearchableText(item);
const matches = searchableText.includes(searchTerm);
if (matches || searchTerm === "") {
this.showItem(item);
} else {
this.hideItem(item);
}
});
this.updateResultsCount();
}
getSearchableText(item) {
// Get text content from data attributes or text content
const name = item.dataset.name || "";
const location = item.dataset.location || "";
const textContent = item.textContent || "";
return (name + " " + location + " " + textContent).toLowerCase();
}
showItem(item) {
item.style.display = "block";
item.classList.remove("fade-out");
item.classList.add("fade-in");
}
hideItem(item) {
item.classList.remove("fade-in");
item.classList.add("fade-out");
setTimeout(() => {
if (item.classList.contains("fade-out")) {
item.style.display = "none";
}
}, QRManager.config.animationDuration);
}
updateResultsCount() {
const visibleItems = this.originalItems.filter(
(item) => item.style.display !== "none"
);
const counter = document.querySelector(".results-counter");
if (counter) {
counter.textContent = `${visibleItems.length} results`;
}
}
}
// Download functionality
class DownloadManager {
static downloadQR(base64Image, filename) {
try {
const link = document.createElement("a");
link.href = "data:image/png;base64," + base64Image;
link.download =
filename.replace(/[^a-z0-9]/gi, "_").toLowerCase() + "_qr_code.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
QRManager.utils.showToast("QR code downloaded successfully!", "success");
} catch (error) {
console.error("Download error:", error);
QRManager.utils.showToast("Failed to download QR code", "error");
}
}
static downloadModalQR() {
if (window.currentModalQR) {
const base64Data = window.currentModalQR.image.split("base64,")[1];
this.downloadQR(base64Data, window.currentModalQR.name);
}
}
}
// Theme management
class ThemeManager {
constructor() {
this.initThemeToggle();
this.loadSavedTheme();
}
initThemeToggle() {
const themeToggle = document.getElementById("themeToggle");
if (themeToggle) {
themeToggle.addEventListener("click", () => {
this.toggleTheme();
});
}
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
this.updateThemeIcon(newTheme);
}
loadSavedTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", savedTheme);
this.updateThemeIcon(savedTheme);
}
updateThemeIcon(theme) {
const themeIcon = document.querySelector("#themeToggle i");
if (themeIcon) {
themeIcon.className = theme === "dark" ? "fas fa-sun" : "fas fa-moon";
}
}
}
// Form validation
class FormValidator {
constructor(formId) {
this.form = document.getElementById(formId);
if (this.form) {
this.init();
}
}
init() {
this.form.addEventListener("submit", (e) => {
if (!this.validateForm()) {
e.preventDefault();
}
});
// Real-time validation
const inputs = this.form.querySelectorAll("input, select, textarea");
inputs.forEach((input) => {
input.addEventListener("blur", () => this.validateField(input));
input.addEventListener("input", () => this.clearFieldError(input));
});
}
validateForm() {
const inputs = this.form.querySelectorAll(
"input[required], select[required], textarea[required]"
);
let isValid = true;
inputs.forEach((input) => {
if (!this.validateField(input)) {
isValid = false;
}
});
return isValid;
}
validateField(field) {
const value = field.value.trim();
const isRequired = field.hasAttribute("required");
const fieldType = field.type;
// Clear previous errors
this.clearFieldError(field);
// Required field validation
if (isRequired && !value) {
this.showFieldError(field, "This field is required");
return false;
}
// Email validation
if (fieldType === "email" && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
this.showFieldError(field, "Please enter a valid email address");
return false;
}
}
// Password validation
if (fieldType === "password" && value) {
if (value.length < 6) {
this.showFieldError(
field,
"Password must be at least 6 characters long"
);
return false;
}
}
return true;
}
showFieldError(field, message) {
field.classList.add("error");
// Remove existing error message
const existingError = field.parentNode.querySelector(".field-error");
if (existingError) {
existingError.remove();
}
// Add new error message
const errorElement = document.createElement("div");
errorElement.className = "field-error";
errorElement.textContent = message;
field.parentNode.appendChild(errorElement);
}
clearFieldError(field) {
field.classList.remove("error");
const errorElement = field.parentNode.querySelector(".field-error");
if (errorElement) {
errorElement.remove();
}
}
}
// Initialize when DOM is loaded
document.addEventListener("DOMContentLoaded", function () {
// Initialize managers
window.navigationManager = new NavigationManager();
window.modalManager = new ModalManager();
window.themeManager = new ThemeManager();
// Initialize search if search input exists
const searchInput =
document.getElementById("searchInput") ||
document.getElementById("qrSearch") ||
document.getElementById("searchUsers");
if (searchInput) {
const containerId = searchInput.dataset.container || "searchResults";
window.searchManager = new SearchManager(searchInput.id, containerId);
}
// Initialize form validation for forms with validation class
const forms = document.querySelectorAll(".validate-form");
forms.forEach((form) => {
new FormValidator(form.id);
});
// Global function assignments for inline event handlers
window.downloadQR = DownloadManager.downloadQR;
window.downloadModalQR = DownloadManager.downloadModalQR;
window.showToast = QRManager.utils.showToast;
});
// Global utility functions
window.QRManager = QRManager;
+739
View File
@@ -0,0 +1,739 @@
/**
* Users management JavaScript functionality
* static/js/users.js
*/
class UsersManager {
constructor() {
this.selectedUsers = new Set();
this.init();
}
init() {
this.initializeSearch();
this.initializeFilters();
this.initializeBulkActions();
this.setupEventListeners();
this.initializeModals();
}
// Initialize search functionality
initializeSearch() {
const searchInput = document.getElementById("searchUsers");
if (!searchInput) return;
let searchTimeout;
searchInput.addEventListener("input", () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.filterUsers();
}, 300);
});
}
// Initialize filter functionality
initializeFilters() {
const filters = ["roleFilter", "statusFilter"];
filters.forEach((filterId) => {
const filter = document.getElementById(filterId);
if (filter) {
filter.addEventListener("change", () => {
this.filterUsers();
});
}
});
}
// Initialize bulk actions
initializeBulkActions() {
const selectAllCheckbox = document.getElementById("selectAllUsers");
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener("change", (e) => {
this.toggleSelectAll(e.target.checked);
});
}
// Individual checkbox handlers
const userCheckboxes = document.querySelectorAll(".user-checkbox");
userCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", (e) => {
this.handleUserSelection(e.target);
});
});
// Bulk action buttons
this.setupBulkActionButtons();
}
setupBulkActionButtons() {
const bulkDeactivateBtn = document.getElementById("bulkDeactivateBtn");
const bulkActivateBtn = document.getElementById("bulkActivateBtn");
const bulkDeleteBtn = document.getElementById("bulkDeleteBtn");
if (bulkDeactivateBtn) {
bulkDeactivateBtn.addEventListener("click", () => {
this.bulkDeactivateUsers();
});
}
if (bulkActivateBtn) {
bulkActivateBtn.addEventListener("click", () => {
this.bulkActivateUsers();
});
}
if (bulkDeleteBtn) {
bulkDeleteBtn.addEventListener("click", () => {
this.bulkDeleteUsers();
});
}
}
// Setup event listeners
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
// ESC to close modals
if (e.key === "Escape") {
this.closeAllModals();
}
// 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();
}
});
// Click outside dropdowns to close
document.addEventListener("click", (e) => {
if (!e.target.closest(".dropdown")) {
this.closeAllDropdowns();
}
});
}
// Initialize modal functionality
initializeModals() {
const modals = document.querySelectorAll(".modal");
modals.forEach((modal) => {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeModal(modal);
}
});
});
}
// Filter users based on search and filters
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");
let visibleCount = 0;
userRows.forEach((row) => {
const name = row.dataset.name?.toLowerCase() || "";
const email = row.dataset.email?.toLowerCase() || "";
const username = row.dataset.username?.toLowerCase() || "";
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) {
this.showUserRow(row);
visibleCount++;
} else {
this.hideUserRow(row);
}
});
this.updateResultsCount(visibleCount);
}
showUserRow(row) {
row.style.display = "table-row";
row.classList.remove("fade-out");
row.classList.add("fade-in");
}
hideUserRow(row) {
row.classList.remove("fade-in");
row.classList.add("fade-out");
setTimeout(() => {
if (row.classList.contains("fade-out")) {
row.style.display = "none";
}
}, 300);
}
updateResultsCount(count) {
const counter = document.querySelector(".results-counter");
if (counter) {
counter.textContent = `${count} users found`;
}
}
// Dropdown management
toggleDropdown(event, button) {
event.stopPropagation();
const dropdown = button.closest(".dropdown");
const menu = dropdown.querySelector(".dropdown-menu");
// Close all other dropdowns
this.closeAllDropdowns();
// Toggle current dropdown
menu.classList.toggle("show");
}
closeAllDropdowns() {
const openMenus = document.querySelectorAll(".dropdown-menu.show");
openMenus.forEach((menu) => {
menu.classList.remove("show");
});
}
// User Actions
async deactivateUser(userId, userName) {
const confirmed = await this.showConfirmation(
"Deactivate User",
`Are you sure you want to deactivate ${userName}?`,
"This will disable their login access but preserve their data."
);
if (!confirmed) return;
try {
const response = await fetch(`/users/${userId}/delete`, {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
if (response.ok) {
// Update UI
this.updateUserStatus(userId, "inactive");
window.showToast(
`User ${userName} deactivated successfully`,
"success"
);
} else {
throw new Error("Failed to deactivate user");
}
} catch (error) {
console.error("Deactivation error:", error);
window.showToast("Failed to deactivate user", "error");
}
}
async reactivateUser(userId, userName) {
try {
const response = await fetch(`/users/${userId}/reactivate`, {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
if (response.ok) {
this.updateUserStatus(userId, "active");
window.showToast(
`User ${userName} reactivated successfully`,
"success"
);
} else {
throw new Error("Failed to reactivate user");
}
} catch (error) {
console.error("Reactivation error:", error);
window.showToast("Failed to reactivate user", "error");
}
}
async promoteUser(userId, userName) {
const confirmed = await this.showConfirmation(
"Promote to Admin",
`Promote ${userName} to admin?`,
"This will give them full system access including user management and system settings."
);
if (!confirmed) return;
try {
const response = await fetch(`/users/${userId}/promote`, {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
if (response.ok) {
this.updateUserRole(userId, "admin");
window.showToast(
`${userName} promoted to admin successfully`,
"success"
);
} else {
throw new Error("Failed to promote user");
}
} catch (error) {
console.error("Promotion error:", error);
window.showToast("Failed to promote user", "error");
}
}
async demoteUser(userId, userName) {
const confirmed = await this.showConfirmation(
"Demote from Admin",
`Demote ${userName} from admin to staff?`,
"This will remove their admin privileges and limit access to QR code management only."
);
if (!confirmed) return;
try {
const response = await fetch(`/users/${userId}/demote`, {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
if (response.ok) {
this.updateUserRole(userId, "staff");
window.showToast(
`${userName} demoted to staff successfully`,
"success"
);
} else {
throw new Error("Failed to demote user");
}
} catch (error) {
console.error("Demotion error:", error);
window.showToast("Failed to demote user", "error");
}
}
async permanentlyDeleteUser(userId, userName) {
const confirmed = await this.showConfirmation(
"Permanently Delete User",
`⚠️ PERMANENTLY DELETE ${userName}?`,
"This action CANNOT be undone and will permanently remove the user account and all associated QR codes.",
"danger"
);
if (!confirmed) return;
try {
const response = await fetch(`/users/${userId}/permanently-delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
});
if (response.ok) {
// Remove user row from table
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
if (userRow) {
userRow.classList.add("fade-out");
setTimeout(() => userRow.remove(), 300);
}
window.showToast(`User ${userName} permanently deleted`, "success");
} else {
throw new Error("Failed to delete user");
}
} catch (error) {
console.error("Deletion error:", error);
window.showToast("Failed to delete user", "error");
}
}
// Update UI after user actions
updateUserStatus(userId, newStatus) {
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
if (!userRow) return;
userRow.dataset.status = newStatus;
const statusBadge = userRow.querySelector(".user-status");
if (statusBadge) {
statusBadge.className = `user-status ${newStatus}`;
statusBadge.innerHTML = `
<i class="fas ${
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
}"></i>
${newStatus === "active" ? "Active" : "Inactive"}
`;
}
// Update action buttons in dropdown
this.updateUserActions(userId, newStatus);
}
updateUserRole(userId, newRole) {
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
if (!userRow) return;
userRow.dataset.role = newRole;
const roleBadge = userRow.querySelector(".user-role");
if (roleBadge) {
roleBadge.className = `user-role ${newRole}`;
roleBadge.textContent = newRole;
}
// Update action buttons
this.updateUserActions(userId, null, newRole);
}
updateUserActions(userId, status = null, role = null) {
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
if (!userRow) return;
const currentStatus = status || userRow.dataset.status;
const currentRole = role || userRow.dataset.role;
// Update dropdown menu items
const dropdownMenu = userRow.querySelector(".dropdown-menu");
if (dropdownMenu) {
// This would update the dropdown items based on new status/role
// Implementation depends on your dropdown structure
}
}
// Bulk Actions
toggleSelectAll(checked) {
const userCheckboxes = document.querySelectorAll(".user-checkbox");
userCheckboxes.forEach((checkbox) => {
checkbox.checked = checked;
this.handleUserSelection(checkbox);
});
}
handleUserSelection(checkbox) {
const userId = checkbox.value;
if (checkbox.checked) {
this.selectedUsers.add(userId);
} else {
this.selectedUsers.delete(userId);
}
this.updateBulkActionsBar();
this.updateSelectAllState();
}
updateBulkActionsBar() {
const bulkActionsBar = document.getElementById("bulkActionsBar");
const selectedCount = document.getElementById("selectedCount");
if (bulkActionsBar && selectedCount) {
if (this.selectedUsers.size > 0) {
bulkActionsBar.classList.add("show");
selectedCount.textContent = this.selectedUsers.size;
} else {
bulkActionsBar.classList.remove("show");
}
}
}
updateSelectAllState() {
const selectAllCheckbox = document.getElementById("selectAllUsers");
const userCheckboxes = document.querySelectorAll(".user-checkbox");
if (selectAllCheckbox && userCheckboxes.length > 0) {
const checkedCount = Array.from(userCheckboxes).filter(
(cb) => cb.checked
).length;
selectAllCheckbox.checked = checkedCount === userCheckboxes.length;
selectAllCheckbox.indeterminate =
checkedCount > 0 && checkedCount < userCheckboxes.length;
}
}
async bulkDeactivateUsers() {
if (this.selectedUsers.size === 0) return;
const confirmed = await this.showConfirmation(
"Bulk Deactivate Users",
`Deactivate ${this.selectedUsers.size} selected users?`,
"This will disable their login access but preserve their data."
);
if (!confirmed) return;
try {
const response = await fetch("/users/bulk/deactivate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
body: JSON.stringify({
user_ids: Array.from(this.selectedUsers),
}),
});
const result = await response.json();
if (result.success) {
// Update UI for deactivated users
this.selectedUsers.forEach((userId) => {
this.updateUserStatus(userId, "inactive");
});
this.clearSelection();
window.showToast(result.message, "success");
} else {
throw new Error(result.message);
}
} catch (error) {
console.error("Bulk deactivation error:", error);
window.showToast("Failed to deactivate users", "error");
}
}
async bulkActivateUsers() {
if (this.selectedUsers.size === 0) return;
const confirmed = await this.showConfirmation(
"Bulk Activate Users",
`Activate ${this.selectedUsers.size} selected users?`,
"This will restore their login access."
);
if (!confirmed) return;
try {
const response = await fetch("/users/bulk/activate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
body: JSON.stringify({
user_ids: Array.from(this.selectedUsers),
}),
});
const result = await response.json();
if (result.success) {
this.selectedUsers.forEach((userId) => {
this.updateUserStatus(userId, "active");
});
this.clearSelection();
window.showToast(result.message, "success");
} else {
throw new Error(result.message);
}
} catch (error) {
console.error("Bulk activation error:", error);
window.showToast("Failed to activate users", "error");
}
}
async bulkDeleteUsers() {
if (this.selectedUsers.size === 0) return;
const confirmed = await this.showConfirmation(
"Permanently Delete Users",
`⚠️ PERMANENTLY DELETE ${this.selectedUsers.size} selected users?`,
"This action CANNOT be undone and will permanently remove all user accounts and their associated data.",
"danger"
);
if (!confirmed) return;
try {
const response = await fetch("/users/bulk/permanently-delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
body: JSON.stringify({
user_ids: Array.from(this.selectedUsers),
}),
});
const result = await response.json();
if (result.success) {
// Remove users from table
this.selectedUsers.forEach((userId) => {
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
if (userRow) {
userRow.classList.add("fade-out");
setTimeout(() => userRow.remove(), 300);
}
});
this.clearSelection();
window.showToast(result.message, "success");
} else {
throw new Error(result.message);
}
} catch (error) {
console.error("Bulk deletion error:", error);
window.showToast("Failed to delete users", "error");
}
}
clearSelection() {
this.selectedUsers.clear();
const userCheckboxes = document.querySelectorAll(".user-checkbox");
userCheckboxes.forEach((checkbox) => {
checkbox.checked = false;
});
this.updateBulkActionsBar();
this.updateSelectAllState();
}
// Modal and confirmation dialogs
showConfirmation(title, message, details = "", type = "warning") {
return new Promise((resolve) => {
const modal = document.createElement("div");
modal.className = "modal";
modal.style.display = "flex";
modal.innerHTML = `
<div class="modal-content confirmation-modal">
<div class="modal-header">
<h3>
<i class="fas ${
type === "danger"
? "fa-exclamation-triangle text-danger"
: "fa-question-circle text-warning"
}"></i>
${title}
</h3>
</div>
<div class="modal-body">
<p><strong>${message}</strong></p>
${details ? `<p class="text-muted">${details}</p>` : ""}
</div>
<div class="modal-footer">
<button class="btn btn-secondary cancel-btn">Cancel</button>
<button class="btn btn-${
type === "danger" ? "danger" : "warning"
} confirm-btn">
<i class="fas fa-check"></i> Confirm
</button>
</div>
</div>
`;
document.body.appendChild(modal);
const cancelBtn = modal.querySelector(".cancel-btn");
const confirmBtn = modal.querySelector(".confirm-btn");
const cleanup = () => modal.remove();
cancelBtn.addEventListener("click", () => {
cleanup();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
cleanup();
resolve(true);
});
modal.addEventListener("click", (e) => {
if (e.target === modal) {
cleanup();
resolve(false);
}
});
});
}
closeModal(modal) {
modal.style.display = "none";
}
closeAllModals() {
const modals = document.querySelectorAll('.modal[style*="flex"]');
modals.forEach((modal) => this.closeModal(modal));
}
// User details modal
showUserDetails(userId) {
// Implementation for showing user details modal
const modal = document.getElementById("userDetailsModal");
if (modal) {
// Populate modal with user data
modal.style.display = "flex";
}
}
closeUserDetailsModal() {
const modal = document.getElementById("userDetailsModal");
if (modal) {
modal.style.display = "none";
}
}
// Password reset modal
showPasswordResetModal(userId) {
const modal = document.getElementById("passwordResetModal");
if (modal) {
modal.style.display = "flex";
}
}
closePasswordResetModal() {
const modal = document.getElementById("passwordResetModal");
if (modal) {
modal.style.display = "none";
}
}
}
// Initialize users manager when DOM is loaded
document.addEventListener("DOMContentLoaded", function () {
window.usersManager = new UsersManager();
// Global functions for inline event handlers
window.toggleDropdown = (event, button) =>
window.usersManager.toggleDropdown(event, button);
window.deactivateUser = (userId, userName) =>
window.usersManager.deactivateUser(userId, userName);
window.reactivateUser = (userId, userName) =>
window.usersManager.reactivateUser(userId, userName);
window.promoteUser = (userId, userName) =>
window.usersManager.promoteUser(userId, userName);
window.demoteUser = (userId, userName) =>
window.usersManager.demoteUser(userId, userName);
window.permanentlyDeleteUser = (userId, userName) =>
window.usersManager.permanentlyDeleteUser(userId, userName);
window.showUserDetails = (userId) =>
window.usersManager.showUserDetails(userId);
window.closeUserDetailsModal = () =>
window.usersManager.closeUserDetailsModal();
window.showPasswordResetModal = (userId) =>
window.usersManager.showPasswordResetModal(userId);
window.closePasswordResetModal = () =>
window.usersManager.closePasswordResetModal();
});