421 lines
21 KiB
PHP
421 lines
21 KiB
PHP
<?php
|
|
require_once 'auth.php';
|
|
|
|
// Check if user is logged in
|
|
if (!is_logged_in()) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__ . '/db/config.php';
|
|
|
|
// Data Fetching for Dashboard
|
|
try {
|
|
$pdo = db();
|
|
|
|
// Key Metrics
|
|
$totalActiveCandidates = $pdo->query("SELECT COUNT(*) FROM candidates WHERE status NOT IN ('Hired', 'Rejected')")->fetchColumn();
|
|
$newCandidatesThisWeek = $pdo->query("SELECT COUNT(*) FROM candidates WHERE created_at >= NOW() - INTERVAL 7 DAY")->fetchColumn();
|
|
|
|
// Assuming tasks with "interview" in the name are interviews
|
|
$interviewsThisWeek = $pdo->query("SELECT COUNT(*) FROM tasks WHERE title LIKE '%interview%' AND due_date >= CURDATE() AND due_date < CURDATE() + INTERVAL 7 DAY")->fetchColumn();
|
|
|
|
$pendingTasks = $pdo->query("SELECT COUNT(*) FROM tasks WHERE status NOT IN ('Completed')")->fetchColumn();
|
|
$overdueTasksCount = $pdo->query("SELECT COUNT(*) FROM tasks WHERE status NOT IN ('Completed') AND due_date < CURDATE()")->fetchColumn();
|
|
|
|
$newHiresThisMonth = $pdo->query("SELECT COUNT(*) FROM candidates WHERE status = 'Hired' AND created_at >= NOW() - INTERVAL 1 MONTH")->fetchColumn();
|
|
|
|
// Recent Activity Feed from workflow_logs
|
|
$recentActivities = $pdo->query("
|
|
SELECT wl.*, w.name as workflow_name, c.name as candidate_name
|
|
FROM workflow_logs wl
|
|
LEFT JOIN workflows w ON wl.workflow_id = w.id
|
|
LEFT JOIN candidates c ON wl.candidate_id = c.id
|
|
ORDER BY wl.executed_at DESC
|
|
LIMIT 10
|
|
")->fetchAll();
|
|
|
|
// At-Risk Items
|
|
$overdueTasks = $pdo->query("
|
|
SELECT t.title, t.due_date, c.name as candidate_name
|
|
FROM tasks t
|
|
LEFT JOIN candidates c ON t.assigned_to = c.id
|
|
WHERE t.status != 'Completed' AND t.due_date < CURDATE()
|
|
ORDER BY t.due_date ASC
|
|
")->fetchAll();
|
|
|
|
$inactiveCandidates = $pdo->query("
|
|
SELECT name, email, created_at
|
|
FROM candidates
|
|
WHERE status = 'Applied' AND created_at <= NOW() - INTERVAL 7 DAY
|
|
")->fetchAll();
|
|
|
|
$incompleteOnboarding = $pdo->query("
|
|
SELECT t.title, c.name as candidate_name, t.due_date
|
|
FROM tasks t
|
|
JOIN candidates c ON t.assigned_to = c.id
|
|
WHERE c.status = 'Hired' AND t.status != 'Completed'
|
|
")->fetchAll();
|
|
|
|
$all_candidates = $pdo->query("SELECT * FROM candidates ORDER BY name ASC")->fetchAll();
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("Dashboard Data Fetch Error: " . $e->getMessage());
|
|
// Initialize variables to prevent errors in the view
|
|
$totalActiveCandidates = $newCandidatesThisWeek = $interviewsThisWeek = $pendingTasks = $overdueTasksCount = $newHiresThisMonth = 0;
|
|
$recentActivities = $overdueTasks = $inactiveCandidates = $incompleteOnboarding = $all_candidates = [];
|
|
}
|
|
|
|
function time_ago($datetime, $full = false) {
|
|
$now = new DateTime;
|
|
$ago = new DateTime($datetime);
|
|
$diff = $now->diff($ago);
|
|
|
|
$diff->w = floor($diff->d / 7);
|
|
$diff->d -= $diff->w * 7;
|
|
|
|
$string = array(
|
|
'y' => 'year',
|
|
'm' => 'month',
|
|
'w' => 'week',
|
|
'd' => 'day',
|
|
'h' => 'hour',
|
|
'i' => 'minute',
|
|
's' => 'second',
|
|
);
|
|
foreach ($string as $k => &$v) {
|
|
if ($diff->$k) {
|
|
$v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
|
|
} else {
|
|
unset($string[$k]);
|
|
}
|
|
}
|
|
|
|
if (!$full) $string = array_slice($string, 0, 1);
|
|
return $string ? implode(', ', $string) . ' ago' : 'just now';
|
|
}
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<title>Executive Dashboard - FinMox Flow</title>
|
|
<meta name="description" content="Executive dashboard for FinMox Flow, providing key metrics and insights for HR and operations.">
|
|
|
|
<meta property="og:title" content="FinMox Flow Dashboard">
|
|
<meta property="og:description" content="Key metrics for HR and operations.">
|
|
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
<style>
|
|
.main-content {
|
|
background-color: #f4f7fc;
|
|
}
|
|
.card {
|
|
border: none;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.07);
|
|
}
|
|
.metric-card .card-body {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.metric-card .icon {
|
|
font-size: 2.5rem;
|
|
margin-right: 1rem;
|
|
color: #0d6efd;
|
|
opacity: 0.7;
|
|
}
|
|
.metric-card .value {
|
|
font-size: 2.25rem;
|
|
font-weight: 700;
|
|
}
|
|
.metric-card .label {
|
|
font-size: 1rem;
|
|
color: #6c757d;
|
|
}
|
|
.metric-card .indicator {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
.quick-actions .btn {
|
|
padding: 0.75rem 1.5rem;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
}
|
|
.activity-feed .activity-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
.activity-feed .activity-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.activity-feed .icon {
|
|
font-size: 1.5rem;
|
|
margin-right: 1rem;
|
|
color: #6c757d;
|
|
}
|
|
.at-risk-section .table {
|
|
margin-bottom: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="header d-flex justify-content-between align-items-center">
|
|
<div class="logo">
|
|
<a href="app.php">
|
|
<img src="assets/pasted-20251120-051320-b2b0cdfa.png" alt="FinMox Logo" style="height: 32px;">
|
|
</a>
|
|
</div>
|
|
<nav class="d-flex align-items-center">
|
|
<a href="app.php" class="btn btn-primary me-2">Home</a>
|
|
<a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
|
|
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
|
|
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<?php echo htmlspecialchars($_SESSION['username']); ?>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
|
<?php if (hasPermission('manage_roles')): ?>
|
|
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<?php endif; ?>
|
|
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="main-content">
|
|
<div class="container-fluid py-4">
|
|
|
|
<!-- TOP SECTION - Key Metrics -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card metric-card h-100">
|
|
<div class="card-body">
|
|
<div class="icon"><i class="bi bi-people-fill"></i></div>
|
|
<div>
|
|
<div class="value"><?php echo $totalActiveCandidates; ?></div>
|
|
<div class="label">Total Active Candidates</div>
|
|
<div class="indicator text-success">+<?php echo $newCandidatesThisWeek; ?> this week</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card metric-card h-100">
|
|
<div class="card-body">
|
|
<div class="icon"><i class="bi bi-calendar-check"></i></div>
|
|
<div>
|
|
<div class="value"><?php echo $interviewsThisWeek; ?></div>
|
|
<div class="label">Interviews This Week</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card metric-card h-100">
|
|
<div class="card-body">
|
|
<div class="icon"><i class="bi bi-list-task"></i></div>
|
|
<div>
|
|
<div class="value"><?php echo $pendingTasks; ?></div>
|
|
<div class="label">Pending Tasks</div>
|
|
<?php if ($overdueTasksCount > 0): ?>
|
|
<div class="indicator text-danger"><?php echo $overdueTasksCount; ?> overdue</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card metric-card h-100">
|
|
<div class="card-body">
|
|
<div class="icon"><i class="bi bi-person-plus-fill"></i></div>
|
|
<div>
|
|
<div class="value"><?php echo $newHiresThisMonth; ?></div>
|
|
<div class="label">New Hires This Month</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MIDDLE SECTION - Quick Actions -->
|
|
<div class="card mb-4">
|
|
<div class="card-body text-center quick-actions">
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCandidateModal"><i class="bi bi-plus-circle me-2"></i>Add New Candidate</button>
|
|
<button class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#addTaskModal"><i class="bi bi-calendar-plus me-2"></i>Schedule Interview</button>
|
|
<a href="#" class="btn btn-secondary"><i class="bi bi-file-earmark-text me-2"></i>View Weekly Report</a>
|
|
<a href="workflows.php" class="btn btn-secondary"><i class="bi bi-diagram-3 me-2"></i>Manage Workflows</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- RECENT ACTIVITY FEED -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-white py-3">
|
|
<h5 class="mb-0">Recent Activity</h5>
|
|
</div>
|
|
<div class="card-body activity-feed">
|
|
<?php if (empty($recentActivities)): ?>
|
|
<p class="text-center text-muted">No recent activity.</p>
|
|
<?php else: ?>
|
|
<?php foreach ($recentActivities as $activity): ?>
|
|
<div class="activity-item">
|
|
<div class="icon"><i class="bi bi-bell"></i></div>
|
|
<div class="flex-grow-1">
|
|
<div>
|
|
<?php
|
|
$action_desc = htmlspecialchars(ucfirst(str_replace('_', ' ', $activity['action'])));
|
|
if ($activity['workflow_name']) {
|
|
echo htmlspecialchars(ucfirst(str_replace('_', ' ', $activity['workflow_name'])));
|
|
} else {
|
|
echo $action_desc;
|
|
}
|
|
if ($activity['candidate_name']) {
|
|
echo " for " . htmlspecialchars($activity['candidate_name']);
|
|
}
|
|
?>
|
|
</div>
|
|
<small class="text-muted"><?php echo time_ago($activity['executed_at']); ?></small>
|
|
</div>
|
|
<a href="workflows.php?highlight=<?php echo $activity['workflow_id']; ?>" class="btn btn-sm btn-outline-secondary">Details</a>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AT-RISK ITEMS -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-white py-3">
|
|
<h5 class="mb-0">At-Risk Items</h5>
|
|
</div>
|
|
<div class="card-body at-risk-section">
|
|
<?php if (!empty($overdueTasks)): ?>
|
|
<h6><i class="bi bi-exclamation-triangle-fill text-danger me-2"></i>Overdue Tasks</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<tbody>
|
|
<?php foreach ($overdueTasks as $task): ?>
|
|
<tr>
|
|
<td><?php echo htmlspecialchars($task['title']); ?></td>
|
|
<td><small><?php echo htmlspecialchars($task['candidate_name']); ?></small></td>
|
|
<td class="text-danger"><small><?php echo date("M d", strtotime($task['due_date'])); ?></small></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($inactiveCandidates)): ?>
|
|
<h6 class="mt-3"><i class="bi bi-person-fill-exclamation text-warning me-2"></i>Inactive Candidates (>7 days)</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<tbody>
|
|
<?php foreach ($inactiveCandidates as $candidate): ?>
|
|
<tr>
|
|
<td><?php echo htmlspecialchars($candidate['name']); ?></td>
|
|
<td><small><?php echo htmlspecialchars($candidate['email']); ?></small></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($incompleteOnboarding)): ?>
|
|
<h6 class="mt-3"><i class="bi bi-clipboard-x-fill text-info me-2"></i>Incomplete Onboarding</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<tbody>
|
|
<?php foreach ($incompleteOnboarding as $task): ?>
|
|
<tr>
|
|
<td><?php echo htmlspecialchars($task['title']); ?></td>
|
|
<td><small><?php echo htmlspecialchars($task['candidate_name']); ?></small></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (empty($overdueTasks) && empty($inactiveCandidates) && empty($incompleteOnboarding)): ?>
|
|
<p class="text-center text-muted mt-3">No at-risk items found. Great job!</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Add Candidate Modal -->
|
|
<div class="modal fade" id="addCandidateModal" tabindex="-1" aria-labelledby="addCandidateModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="addCandidateModalLabel">Add New Candidate</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form method="POST" action="app.php">
|
|
<input type="hidden" name="add_candidate" value="1">
|
|
<div class="mb-3"><label for="name" class="form-label">Name</label><input type="text" class="form-control" id="name" name="name" required></div>
|
|
<div class="mb-3"><label for="email" class="form-label">Email</label><input type="email" class="form-control" id="email" name="email" required></div>
|
|
<div class="mb-3"><label for="phone" class="form-label">Phone</label><input type="text" class="form-control" id="phone" name="phone"></div>
|
|
<div class="mb-3"><label for="status" class="form-label">Status</label><select class="form-select" id="status" name="status"><option value="Applied" selected>Applied</option><option value="Interviewing">Interviewing</option><option value="Offered">Offered</option><option value="Hired">Hired</option><option value="Rejected">Rejected</option></select></div>
|
|
<div class="mb-3"><label for="notes" class="form-label">Notes</label><textarea class="form-control" id="notes" name="notes" rows="3"></textarea></div>
|
|
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button><button type="submit" class="btn btn-primary">Save Candidate</button></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Task Modal (for "Schedule Interview") -->
|
|
<div class="modal fade" id="addTaskModal" tabindex="-1" aria-labelledby="addTaskModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="addTaskModalLabel">Schedule Interview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form method="POST" action="app.php">
|
|
<input type="hidden" name="add_task" value="1">
|
|
<div class="mb-3"><label for="task_name" class="form-label">Interview Title</label><input type="text" class="form-control" id="task_name" name="title" required value="Interview with "></div>
|
|
<div class="mb-3"><label for="candidate_id" class="form-label">Candidate</label><select class="form-select" id="candidate_id" name="assigned_to" required><option value="" disabled selected>Select a candidate</option><?php foreach ($all_candidates as $candidate): ?><option value="<?php echo $candidate['id']; ?>"><?php echo htmlspecialchars($candidate['name']); ?></option><?php endforeach; ?></select></div>
|
|
<div class="mb-3"><label for="due_date" class="form-label">Interview Date</label><input type="date" class="form-control" id="due_date" name="due_date" required></div>
|
|
<div class="mb-3"><label for="task_description" class="form-label">Notes</label><textarea class="form-control" id="task_description" name="description" rows="3"></textarea></div>
|
|
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button><button type="submit" class="btn btn-primary">Schedule</button></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
</body>
|
|
</html>
|