Initial Codes
This commit is contained in:
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user