Sindbad~EG File Manager
<?php
require_once '../config/config.php';
require_once '../includes/email_functions_improved.php';
// Check if user is logged in
if (!isLoggedIn()) {
redirect('login.php');
}
$db = new Database();
$conn = $db->getConnection();
// Get dashboard statistics
$stats = [];
// Total attendance records
$query = "SELECT COUNT(*) as total FROM attendance_records";
$stmt = $conn->prepare($query);
$stmt->execute();
$stats['total_attendance'] = $stmt->fetch()['total'];
// Today's attendance
$query = "SELECT COUNT(*) as today FROM attendance_records WHERE DATE(submitted_at) = CURDATE()";
$stmt = $conn->prepare($query);
$stmt->execute();
$stats['today_attendance'] = $stmt->fetch()['today'];
// Active programs
$query = "SELECT COUNT(*) as active FROM programs WHERE is_active = 1";
$stmt = $conn->prepare($query);
$stmt->execute();
$stats['active_programs'] = $stmt->fetch()['active'];
// Total users (for superuser only)
if (hasRole('superuser')) {
$query = "SELECT COUNT(*) as users FROM users WHERE is_active = 1";
$stmt = $conn->prepare($query);
$stmt->execute();
$stats['total_users'] = $stmt->fetch()['users'];
}
// Recent attendance records
$location_filter = '';
$params = [];
if (hasRole('admin') && isset($_SESSION['location_id']) && $_SESSION['location_id']) {
$location_filter = " AND p.location_id = ?";
$params[] = $_SESSION['location_id'];
}
$query = "SELECT ar.*, p.name as program_name, l.name as location_name
FROM attendance_records ar
JOIN programs p ON ar.program_id = p.id
LEFT JOIN locations l ON p.location_id = l.id
WHERE 1=1 $location_filter
ORDER BY ar.submitted_at DESC LIMIT 10";
$stmt = $conn->prepare($query);
$stmt->execute($params);
$recent_attendance = $stmt->fetchAll();
// Attendance by program (last 30 days)
$query = "SELECT p.name, COUNT(ar.id) as count
FROM programs p
LEFT JOIN attendance_records ar ON p.id = ar.program_id AND ar.submitted_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
WHERE p.is_active = 1 $location_filter
GROUP BY p.id, p.name
ORDER BY count DESC LIMIT 5";
$stmt = $conn->prepare($query);
$stmt->execute($params);
$program_stats = $stmt->fetchAll();
// Get email statistics (for superusers only)
$email_stats = [];
if (hasRole('superuser')) {
$email_stats = getEmailStats($conn);
}
// Get site settings
$query = "SELECT setting_key, setting_value FROM settings WHERE setting_key IN ('site_title', 'site_logo')";
$stmt = $conn->prepare($query);
$stmt->execute();
$settings = [];
while ($row = $stmt->fetch()) {
$settings[$row['setting_key']] = $row['setting_value'];
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - <?php echo $settings['site_title'] ?? SITE_TITLE; ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#F59E0B',
accent: '#6B7280'
}
}
}
}
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.gradient-bg {
background: linear-gradient(135deg, #3B82F6 0%, #F59E0B 50%, #6B7280 100%);
}
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3B82F6;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Custom scrollbar styles */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #CBD5E1 #F1F5F9;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #F1F5F9;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #CBD5E1;
border-radius: 3px;
transition: background 0.2s ease;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94A3B8;
}
/* Smooth scroll behavior */
.custom-scrollbar {
scroll-behavior: smooth;
}
/* Loading animation for better UX */
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Chart container styles to prevent overflow issues */
.chart-container {
position: relative;
height: 320px;
width: 100%;
overflow: hidden;
}
.chart-container canvas {
position: absolute;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
}
</style>
</head>
<body class="bg-gray-50">
<!-- Include Sidebar -->
<?php include 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="md:ml-64">
<!-- Header -->
<header class="bg-white shadow-sm border-b">
<div class="px-6 py-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500">
Last login: <?php echo date('M j, Y g:i A'); ?>
</span>
<a href="../index.php" target="_blank" class="text-primary hover:text-blue-700">
<i class="fas fa-external-link-alt mr-1"></i>
View Site
</a>
</div>
</div>
</div>
</header>
<!-- Dashboard Content -->
<main class="p-6">
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<i class="fas fa-users text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Attendance</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($stats['total_attendance']); ?></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<i class="fas fa-calendar-day text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Today's Attendance</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($stats['today_attendance']); ?></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 rounded-lg">
<i class="fas fa-calendar text-yellow-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Active Programs</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($stats['active_programs']); ?></p>
</div>
</div>
</div>
<?php if (hasRole('superuser')): ?>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-purple-100 rounded-lg">
<i class="fas fa-user-cog text-purple-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($stats['total_users']); ?></p>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Email Statistics (Superuser Only) -->
<?php if (hasRole('superuser') && !empty($email_stats)): ?>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<i class="fas fa-envelope text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Emails Sent Today</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($email_stats['today_sent']); ?></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-red-100 rounded-lg">
<i class="fas fa-envelope-open text-red-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Failed Today</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($email_stats['today_failed']); ?></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<i class="fas fa-chart-line text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">This Month</p>
<p class="text-2xl font-bold text-gray-900"><?php echo number_format($email_stats['month_sent']); ?></p>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Program Statistics Chart -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
<i class="fas fa-chart-bar mr-2 text-primary"></i>
Program Attendance (Last 30 Days)
</h3>
<div class="chart-container">
<canvas id="programChart"></canvas>
</div>
</div>
<!-- Recent Attendance -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">
<i class="fas fa-clock mr-2 text-primary"></i>
Recent Attendance
</h3>
<span class="text-sm text-gray-500">Latest submissions</span>
</div>
</div>
<div class="relative">
<div id="attendance-container" class="h-80 overflow-y-auto custom-scrollbar">
<?php if (empty($recent_attendance)): ?>
<div id="no-records" class="flex items-center justify-center h-full">
<div class="text-center">
<i class="fas fa-users text-4xl text-gray-300 mb-3"></i>
<p class="text-gray-500">No recent attendance records</p>
</div>
</div>
<?php else: ?>
<div class="divide-y divide-gray-100">
<?php foreach ($recent_attendance as $record): ?>
<div class="attendance-record px-6 py-4 hover:bg-gray-50 transition-colors duration-150">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center">
<span class="text-white text-sm font-medium">
<?php echo strtoupper(substr($record['full_name'], 0, 1)); ?>
</span>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
<?php echo htmlspecialchars($record['full_name']); ?>
</p>
<p class="text-sm text-gray-500 truncate">
<?php echo htmlspecialchars($record['program_name']); ?>
</p>
</div>
</div>
</div>
<div class="flex-shrink-0 text-right">
<p class="text-xs text-gray-500">
<?php echo date('M j, g:i A', strtotime($record['submitted_at'])); ?>
</p>
<?php if ($record['location_name']): ?>
<p class="text-xs text-gray-400 truncate max-w-24">
<?php echo htmlspecialchars($record['location_name']); ?>
</p>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Loading indicator -->
<div id="loading-indicator" class="hidden px-6 py-4 text-center border-t border-gray-100">
<div class="inline-flex items-center text-sm text-gray-500">
<div class="spinner mr-2"></div>
Loading more records...
</div>
</div>
<!-- Load More Button (fallback) -->
<div id="load-more-btn" class="hidden px-6 py-4 text-center border-t border-gray-100">
<button onclick="loadMoreRecords()"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-primary bg-primary/10 rounded-lg hover:bg-primary/20 transition-colors duration-150">
<i class="fas fa-plus mr-2"></i>
Load More Records
</button>
</div>
<!-- End of records indicator -->
<div id="end-indicator" class="hidden px-6 py-4 text-center border-t border-gray-100">
<p class="text-sm text-gray-400">
<i class="fas fa-check-circle mr-2"></i>
All records loaded
</p>
</div>
</div>
<!-- Scroll indicator -->
<div class="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-t from-white to-transparent pointer-events-none"></div>
</div>
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
<a href="attendance.php" class="inline-flex items-center text-sm font-medium text-primary hover:text-blue-700 transition-colors duration-150">
View All Records
<i class="fas fa-arrow-right ml-2"></i>
</a>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="mt-8">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="programs.php?action=create" class="bg-white rounded-lg shadow p-4 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<i class="fas fa-plus text-blue-600"></i>
</div>
<div class="ml-3">
<p class="font-medium text-gray-900">Create Program</p>
<p class="text-sm text-gray-500">Add a new program</p>
</div>
</div>
</a>
<a href="reports.php" class="bg-white rounded-lg shadow p-4 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<i class="fas fa-download text-green-600"></i>
</div>
<div class="ml-3">
<p class="font-medium text-gray-900">Generate Report</p>
<p class="text-sm text-gray-500">Export attendance data</p>
</div>
</div>
</a>
<a href="forms.php" class="bg-white rounded-lg shadow p-4 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 rounded-lg">
<i class="fas fa-edit text-yellow-600"></i>
</div>
<div class="ml-3">
<p class="font-medium text-gray-900">Manage Forms</p>
<p class="text-sm text-gray-500">Edit form templates</p>
</div>
</div>
</a>
</div>
</div>
</main>
</div>
<script>
// Mobile menu functionality is handled by sidebar.php
// Program statistics chart
const ctx = document.getElementById('programChart').getContext('2d');
const programData = <?php echo json_encode($program_stats); ?>;
new Chart(ctx, {
type: 'bar',
data: {
labels: programData.map(item => item.name),
datasets: [{
label: 'Attendance Count',
data: programData.map(item => item.count),
backgroundColor: 'rgba(59, 130, 246, 0.8)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
scales: {
x: {
display: true,
grid: {
display: false
}
},
y: {
beginAtZero: true,
display: true,
grid: {
color: 'rgba(0, 0, 0, 0.1)'
},
ticks: {
stepSize: 1,
precision: 0
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: 'white',
bodyColor: 'white',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1
}
},
layout: {
padding: {
top: 10,
bottom: 10
}
}
}
});
// Dashboard Infinite Scrolling Implementation
let dashboardCurrentPage = 2; // Start from page 2 since page 1 is already loaded
let dashboardIsLoading = false;
let dashboardHasMore = true;
function initDashboardInfiniteScroll() {
const container = document.getElementById('attendance-container');
const loadingIndicator = document.getElementById('loading-indicator');
const endIndicator = document.getElementById('end-indicator');
const loadMoreBtn = document.getElementById('load-more-btn');
if (!container) {
console.error('Attendance container not found');
return;
}
console.log('Initializing dashboard infinite scroll...');
// Check if we have initial records
const initialRecords = container.querySelectorAll('.attendance-record').length;
console.log('Initial records found:', initialRecords);
// Show load more button if we have records and potentially more
if (initialRecords > 0 && loadMoreBtn) {
loadMoreBtn.classList.remove('hidden');
}
// Add scroll listener with throttling
let scrollTimeout;
container.addEventListener('scroll', function() {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (dashboardIsLoading || !dashboardHasMore) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// Load more when near bottom (20px threshold - less aggressive)
if (scrollTop + clientHeight >= scrollHeight - 20) {
console.log('Scroll threshold reached, loading more...');
loadMoreDashboardRecords();
}
}, 100);
});
// Make function global for button
window.loadMoreRecords = loadMoreDashboardRecords;
// Removed auto-loading to prevent unwanted scrolling behavior
}
async function loadMoreDashboardRecords() {
if (dashboardIsLoading || !dashboardHasMore) {
console.log('Dashboard load blocked:', { isLoading: dashboardIsLoading, hasMore: dashboardHasMore });
return;
}
console.log('Loading dashboard page:', dashboardCurrentPage);
dashboardIsLoading = true;
// Show loading
const loadingIndicator = document.getElementById('loading-indicator');
const loadMoreBtn = document.getElementById('load-more-btn');
const endIndicator = document.getElementById('end-indicator');
if (loadingIndicator) loadingIndicator.classList.remove('hidden');
if (loadMoreBtn) loadMoreBtn.classList.add('hidden');
try {
const response = await fetch(`api/get_attendance.php?page=${dashboardCurrentPage}&limit=10`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Dashboard API Response:', data);
if (data.success && data.records) {
addDashboardRecordsToContainer(data.records);
dashboardHasMore = data.pagination.has_more;
dashboardCurrentPage++;
console.log(`Added ${data.records.length} dashboard records. Has more: ${dashboardHasMore}`);
if (!dashboardHasMore) {
if (endIndicator) endIndicator.classList.remove('hidden');
} else {
if (loadMoreBtn) loadMoreBtn.classList.remove('hidden');
}
} else {
console.error('Dashboard API Error:', data.message || 'Unknown error');
showDashboardError('Failed to load records: ' + (data.message || 'Unknown error'));
if (loadMoreBtn) loadMoreBtn.classList.remove('hidden');
}
} catch (error) {
console.error('Dashboard Network Error:', error);
showDashboardError('Network error: ' + error.message);
if (loadMoreBtn) loadMoreBtn.classList.remove('hidden');
} finally {
dashboardIsLoading = false;
if (loadingIndicator) loadingIndicator.classList.add('hidden');
}
}
function addDashboardRecordsToContainer(records) {
const container = document.getElementById('attendance-container');
const loadingIndicator = document.getElementById('loading-indicator');
if (!container || !records.length) return;
// Hide "no records" message if present
const noRecords = document.getElementById('no-records');
if (noRecords) noRecords.parentElement.style.display = 'none';
// Find or create the records container
let recordsContainer = container.querySelector('.divide-y');
if (!recordsContainer) {
recordsContainer = document.createElement('div');
recordsContainer.className = 'divide-y divide-gray-100';
container.insertBefore(recordsContainer, loadingIndicator);
}
records.forEach(record => {
const recordDiv = document.createElement('div');
recordDiv.className = 'attendance-record px-6 py-4 hover:bg-gray-50 transition-colors duration-150';
// Create the main container
const mainDiv = document.createElement('div');
mainDiv.className = 'flex items-center justify-between';
// Left side with avatar and info
const leftDiv = document.createElement('div');
leftDiv.className = 'flex-1 min-w-0';
const flexDiv = document.createElement('div');
flexDiv.className = 'flex items-center space-x-3';
// Avatar
const avatarDiv = document.createElement('div');
avatarDiv.className = 'flex-shrink-0';
const avatar = document.createElement('div');
avatar.className = 'w-8 h-8 bg-primary rounded-full flex items-center justify-center';
const avatarText = document.createElement('span');
avatarText.className = 'text-white text-sm font-medium';
avatarText.textContent = record.full_name.charAt(0).toUpperCase();
avatar.appendChild(avatarText);
avatarDiv.appendChild(avatar);
// Info section
const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1 min-w-0';
const nameP = document.createElement('p');
nameP.className = 'text-sm font-medium text-gray-900 truncate';
nameP.textContent = record.full_name;
const programP = document.createElement('p');
programP.className = 'text-sm text-gray-500 truncate';
programP.textContent = record.program_name;
infoDiv.appendChild(nameP);
infoDiv.appendChild(programP);
flexDiv.appendChild(avatarDiv);
flexDiv.appendChild(infoDiv);
leftDiv.appendChild(flexDiv);
// Right side with date/location
const rightDiv = document.createElement('div');
rightDiv.className = 'flex-shrink-0 text-right';
const dateP = document.createElement('p');
dateP.className = 'text-xs text-gray-500';
dateP.textContent = record.formatted_date;
rightDiv.appendChild(dateP);
if (record.location_name) {
const locationP = document.createElement('p');
locationP.className = 'text-xs text-gray-400 truncate max-w-24';
locationP.textContent = record.location_name;
rightDiv.appendChild(locationP);
}
mainDiv.appendChild(leftDiv);
mainDiv.appendChild(rightDiv);
recordDiv.appendChild(mainDiv);
// Add to records container
recordsContainer.appendChild(recordDiv);
});
}
function showDashboardError(message) {
const container = document.getElementById('attendance-container');
if (!container) return;
// Remove existing error
const existingError = document.getElementById('dashboard-error-message');
if (existingError) existingError.remove();
// Create error element
const errorDiv = document.createElement('div');
errorDiv.id = 'dashboard-error-message';
errorDiv.className = 'text-center py-4';
errorDiv.innerHTML = `
<p class="text-red-500 text-sm">
<i class="fas fa-exclamation-triangle mr-1"></i>
${message}
</p>
`;
container.appendChild(errorDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.remove();
}
}, 5000);
}
// Initialize when page loads
initDashboardInfiniteScroll();
</script>
</body>
</html>
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists