COMPANY_DASHBOARD / templates /company_dashboard.html
pranit144's picture
Upload 12 files
1eabbf4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Company Dashboard | Campus Recruitment Portal</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- Link to your EXTERNAL static CSS file -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
/* Minimal page-specific overrides */
.chart-container {
position: relative;
height: 280px; /* Default height */
width: 100%;
min-height: 200px; /* Ensure minimum height */
}
canvas {
display: block; /* Prevents extra space below canvas */
max-width: 100%; /* Ensure canvas doesn't overflow container */
}
/* Make table header sticky */
.table-responsive thead {
position: sticky;
top: 0;
z-index: 1;
/* Background applied by .table-light in the thead tag */
}
.filter-section .card-body {
max-height: calc(100vh - 150px); /* Adjust based on nav height etc. */
overflow-y: auto;
}
@media (max-width: 992px) {
.filter-section .card-body {
max-height: none;
overflow-y: visible;
}
}
/* Style for AI Query section */
#nl-query-error {
font-size: 0.85rem;
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<i class="bi bi-building"></i> {{ company_name }} - Portal
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('dashboard') }}"><i class="bi bi-speedometer2"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('jobs') }}"><i class="bi bi-briefcase-fill"></i> Job Postings</a>
</li>
</ul>
<ul class="navbar-nav">
<!-- Display Flash Messages Dropdown -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-warning position-relative" href="#" id="messagesDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-bell-fill"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6em;">
{{ messages|length }}
<span class="visually-hidden">notifications</span>
</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2" aria-labelledby="messagesDropdown" style="min-width: 250px;">
<li><h6 class="dropdown-header small text-muted">Notifications</h6></li>
{% for category, message in messages %}
<li>
<div class="dropdown-item small py-2">
<span class="badge bg-{{ category if category in ['danger', 'warning', 'info', 'success'] else 'secondary' }} me-1"> </span>
{{ message }}
</div>
</li>
{% endfor %}
<li><hr class="dropdown-divider my-1"></li>
<li><a class="dropdown-item small text-muted text-center" href="#">View All (Not implemented)</a></li>
</ul>
</li>
{% endif %}
{% endwith %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}" title="Logout">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container-fluid mt-3 main-container">
<!-- Display top-level error if passed -->
{% if error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ error }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row">
<!-- Filters Column -->
<div class="col-lg-3">
<!-- Filters Card -->
<div class="card mb-3 filter-section shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="card-title mb-0"><i class="bi bi-filter"></i> Filter Candidates</h5>
</div>
<div class="card-body">
<form id="filter-form">
<!-- Row for Job Filter (Spans full width initially for emphasis) -->
<div class="row mb-3">
<div class="col-12 filter-group border-bottom pb-3">
<label for="job_id_filter" class="form-label filter-heading">Filter by Job</label>
<select class="form-select form-select-sm" name="job_id" id="job_id_filter">
<option value="">(All Candidates / Use Manual Filters)</option>
{% for job in jobs %}
<option value="{{ job.id }}">{{ job.title }}</option>
{% endfor %}
</select>
<small class="form-text text-muted">Applies job's requirements & overrides manual filters below if selected.</small>
</div>
</div>
<!-- Row for the rest of the filters (2 columns on large screens) -->
<div class="row">
<!-- Column 1: Basic Criteria & Experience -->
<div class="col-12 col-lg-6">
<div class="mb-3 filter-group">
<label for="min_cgpa_filter" class="form-label filter-heading">Min CGPA</label>
<input type="number" class="form-control form-control-sm" name="min_cgpa" id="min_cgpa_filter" min="0" max="10" step="0.1" placeholder="e.g., 7.5">
</div>
<div class="mb-3 filter-group">
<label for="max_backlogs_filter" class="form-label filter-heading">Max Backlogs</label>
<input type="number" class="form-control form-control-sm" name="max_backlogs" id="max_backlogs_filter" min="0" step="1" placeholder="e.g., 0">
</div>
<div class="mb-3 filter-group">
<label class="form-label filter-heading">Experience Flags</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="has_hackathons" id="hasHackathons">
<label class="form-check-label" for="hasHackathons">Has Hackathon</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="has_experience" id="hasExperience">
<label class="form-check-label" for="hasExperience">Has Project/Internship</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="has_research" id="hasResearch">
<label class="form-check-label" for="hasResearch">Has Research/Pubs</label>
</div>
</div>
</div><!-- End Column 1 -->
<!-- Column 2: Skills, Roles, Certifications -->
<div class="col-12 col-lg-6">
<div class="mb-3 filter-group">
<label for="skills_filter" class="form-label filter-heading">Skills (any)</label>
<select class="form-select form-select-sm" name="skills" id="skills_filter" multiple size="5" aria-label="Select skills">
<!-- Combine programming languages and tools -->
{% set all_skills = filter_options.programming_languages + filter_options.tools %}
{% for skill in all_skills | unique | sort %}
<option value="{{ skill }}">{{ skill }}</option>
{% endfor %}
</select>
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple</small>
</div>
<div class="mb-3 filter-group">
<label for="roles_filter" class="form-label filter-heading">Preferred Roles (any)</label>
<select class="form-select form-select-sm" name="roles" id="roles_filter" multiple size="5" aria-label="Select preferred roles">
{% for role in filter_options.roles %}
<option value="{{ role }}">{{ role }}</option>
{% endfor %}
</select>
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple</small>
</div>
<div class="mb-3 filter-group">
<label for="certifications_filter" class="form-label filter-heading">Certifications (any)</label>
<select class="form-select form-select-sm" name="certifications" id="certifications_filter" multiple size="5" aria-label="Select certifications">
{% for cert in filter_options.certifications %}
<option value="{{ cert }}">{{ cert }}</option>
{% endfor %}
</select>
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple</small>
</div>
</div><!-- End Column 2 -->
</div><!-- End Row for columns -->
<!-- Buttons (Placed after the columns row) -->
<div class="d-grid gap-2 mt-3">
<button type="submit" class="btn btn-primary"><i class="bi bi-funnel-fill"></i> Apply Filters</button>
<button type="reset" class="btn btn-outline-secondary" id="reset-filters"><i class="bi bi-arrow-clockwise"></i> Reset Filters</button>
</div>
</form>
</div><!-- End card-body -->
</div><!-- End Filters Card -->
<!-- Export Options Card -->
<div class="card mb-3 shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="card-title mb-0"><i class="bi bi-download"></i> Export Options</h5>
</div>
<div class="card-body">
<div class="d-grid">
<button type="button" class="btn btn-success" id="export-csv-btn" disabled>
<i class="bi bi-file-earmark-spreadsheet"></i> Export Filtered to CSV
</button>
</div>
<small class="form-text text-muted d-block mt-2">Exports the students currently shown in the table.</small>
<form id="export-form" class="d-none" action="{{ url_for('export_filtered') }}" method="post" target="_blank">
<input type="hidden" name="filtered_students" id="filtered-students-data-hidden">
</form>
</div>
</div>
</div> <!-- End Filters Column -->
<!-- Main Content Area -->
<div class="col-lg-9">
<!-- Natural Language Query Section -->
<div class="card mb-3 shadow-sm">
<div class="card-header bg-light text-dark">
<h5 class="card-title mb-0"><i class="bi bi-robot"></i> Query Candidates with AI</h5>
</div>
<div class="card-body">
<div id="nl-query-error" class="alert alert-danger d-none p-2" role="alert"></div>
<div class="mb-2">
<label for="nl-query-input" class="form-label small">Enter your query (e.g., "Show Computer Science students with CGPA over 8 and Python skill, must have internships")</label>
<textarea class="form-control form-control-sm" id="nl-query-input" rows="2" placeholder="Describe the candidates you're looking for..."></textarea>
</div>
<button type="button" class="btn btn-info btn-sm" id="nl-query-submit">
<i class="bi bi-magic"></i> Generate & Apply Filters
</button>
<span id="nl-query-spinner" class="spinner-border spinner-border-sm text-info ms-2 d-none" role="status" aria-hidden="true"></span>
<small class="text-muted d-block mt-1">AI will attempt to translate your query into the filters on the left.</small>
</div>
</div>
<!-- End Natural Language Query Section -->
<!-- Visualization Section -->
<div class="card mb-3 shadow-sm">
<div class="card-header bg-light text-dark d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-bar-chart-line-fill"></i> Data Visualizations <small class="text-muted">(Updates with filters)</small></h5>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#visualizationsCollapse" aria-expanded="true" aria-controls="visualizationsCollapse" id="collapseVizButton">
<i class="bi bi-chevron-up"></i> <span>Collapse</span>
</button>
</div>
<div class="collapse show" id="visualizationsCollapse">
<div class="card-body">
<div class="row">
<div class="col-xl-6 col-lg-12 mb-3">
<div class="card visualization-card h-100 border">
<div class="card-header small">CGPA Distribution</div>
<div class="card-body chart-container">
<canvas id="cgpaChart"></canvas>
</div>
</div>
</div>
<div class="col-xl-6 col-lg-12 mb-3">
<div class="card visualization-card h-100 border">
<div class="card-header small">Department Distribution</div>
<div class="card-body chart-container">
<canvas id="deptChart"></canvas>
</div>
</div>
</div>
<div class="col-xl-6 col-lg-12 mb-3">
<div class="card visualization-card h-100 border">
<div class="card-header small">Top Programming Skills</div>
<div class="card-body chart-container">
<canvas id="skillsChart"></canvas>
</div>
</div>
</div>
<div class="col-xl-6 col-lg-12 mb-3">
<div class="card visualization-card h-100 border">
<div class="card-header small">Top Preferred Roles</div>
<div class="card-body chart-container">
<canvas id="rolesChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Students Table Card -->
<div class="card mb-3 shadow-sm">
<div class="card-header bg-light text-dark d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-people-fill"></i> Students List <span id="student-count" class="badge bg-primary rounded-pill">{{ students|length }}</span>
</h5>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" id="select-all" title="Select all visible students"><i class="bi bi-check-square"></i> Select All</button>
<button class="btn btn-outline-primary" id="deselect-all" title="Deselect all visible students"><i class="bi bi-square"></i> Deselect All</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm mb-0">
<thead class="table-light">
<tr>
<th class="text-center" style="width: 3%;"><input type="checkbox" id="select-all-checkbox" class="form-check-input"></th>
<th style="width: 20%;">Name</th>
<th style="width: 15%;">Department</th>
<th style="width: 7%;">CGPA</th>
<th style="width: 7%;">Backlogs</th>
<th style="width: 28%;">Top Skills</th>
<th style="width: 10%;">Experience</th>
</tr>
</thead>
<tbody id="students-tbody">
<!-- Student rows populated by initial Flask render AND JS updates -->
{% for student in students %}
<tr data-email="{{ student.email }}">
<td class="text-center"><input type="checkbox" class="form-check-input student-checkbox" value="{{ student.email }}"></td>
<td>{{ student.full_name | default('N/A', true) }}</td>
<td>{{ student.department | default('N/A', true) }}</td>
<td>{{ "%.2f"|format(student.cgpa) if student.cgpa is not none else 'N/A' }}</td>
<td>{{ student.backlogs | default(0, true) }}</td>
<td>
{# --- CORRECTED SKILL PROCESSING --- #}
{% set raw_skills = [] %}
{% if student.programming_languages %}
{% if student.programming_languages is string %}
{% set raw_skills = student.programming_languages.split(',') %}
{% elif student.programming_languages is iterable and student.programming_languages is not string %} {# Check if it's list-like but not a string #}
{% set raw_skills = student.programming_languages %}
{% endif %}
{% endif %}
{# Use namespace to collect unique, cleaned skills #}
{% set ns = namespace(unique_cleaned_skills=[]) %}
{% for skill in raw_skills %}
{% set trimmed_skill = skill | trim %}
{% if trimmed_skill %}
{# Basic removal of "(...)" suffix #}
{% set base_skill = trimmed_skill.split('(')[0] | trim %}
{% if base_skill and base_skill not in ns.unique_cleaned_skills %}
{# Append to the list within the namespace #}
{% set ns.unique_cleaned_skills = ns.unique_cleaned_skills + [base_skill] %}
{% endif %}
{% endif %}
{% endfor %}
<small>
{# Display top 3 unique, cleaned skills #}
{{ ns.unique_cleaned_skills[:3] | join(', ') }}
{% if ns.unique_cleaned_skills | length > 3 %}...{% endif %}
{% if not ns.unique_cleaned_skills %}N/A{% endif %} {# Show N/A if no skills found #}
</small>
{# --- END OF CORRECTION --- #}
</td>
<td>
{% if student.internships %}<span class="badge bg-success" title="Internship">I</span>{% endif %}
{% if student.projects %}<span class="badge bg-primary" title="Projects">P</span>{% endif %}
{% if student.hackathons %}<span class="badge bg-warning text-dark" title="Hackathon">H</span>{% endif %}
{% if student.publications %}<span class="badge bg-info text-dark" title="Publication/Research">R</span>{% endif %}
{% if not student.internships and not student.projects and not student.hackathons and not student.publications %}
<span class="text-muted small">None</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="no-students-message" class="alert alert-info m-3 {% if students %}d-none{% endif %}">
No students match the current filters. Try resetting or modifying filters.
</div>
</div>
</div>
<!-- AI Insights Section -->
<div class="card mb-3 shadow-sm">
<div class="card-header bg-light text-dark">
<h5 class="card-title mb-0"><i class="bi bi-robot"></i> Gemini-Powered Candidate Insights</h5>
</div>
<div class="card-body">
<form id="ai-insights-form" class="mb-3">
<div class="row g-2 align-items-end">
<div class="col-md-6">
<label for="ai-role-input" class="form-label small mb-1">Job Role for Analysis</label>
<input type="text" class="form-control form-control-sm" id="ai-role-input" name="role" placeholder="e.g., Software Engineer Intern" required>
</div>
<div class="col-md-6">
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-sm" id="generate-insights-btn" disabled>
<i class="bi bi-magic"></i> Generate Insights for Selected (<span id="selected-count">0</span>)
</button>
</div>
</div>
</div>
<small class="text-muted d-block mt-1">Select students from the table above first.</small>
</form>
<div id="insights-result" class="p-3 border rounded bg-light d-none">
<h6 class="mb-2">AI Analysis Results <span id="analysis-role" class="text-muted small"></span></h6>
<div id="insights-spinner" class="text-center d-none my-3">
<div class="spinner-border text-primary spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-1 mb-0 small text-muted">Generating insights...</p>
</div>
<div id="insights-content" class="overflow-auto" style="max-height: 450px; white-space: pre-wrap; font-size: 0.9rem; line-height: 1.6;">
<!-- Insights will be loaded here -->
</div>
<div id="insights-error" class="alert alert-danger small p-2 d-none"></div>
</div>
</div>
</div>
</div> <!-- End Main Content Area Col -->
</div> <!-- End Row -->
</div> <!-- End Main Container -->
<!-- JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<!-- Store initial data passed from Flask -->
<script>
const initialChartData = {{ initial_chart_data|tojson }};
// Store the initial full student data (list of dicts) directly if needed, otherwise it's rendered in the table
let allStudentsInitially = {{ students|tojson|safe }};
</script>
<!-- Custom JS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log("Dashboard DOM Loaded.");
// --- DOM Elements ---
const filterForm = document.getElementById('filter-form');
const resetButton = document.getElementById('reset-filters');
const studentsTableBody = document.getElementById('students-tbody');
const studentCountBadge = document.getElementById('student-count');
const noStudentsMessage = document.getElementById('no-students-message');
const exportCsvBtn = document.getElementById('export-csv-btn');
const aiInsightsForm = document.getElementById('ai-insights-form');
const aiRoleInput = document.getElementById('ai-role-input');
const insightsResultDiv = document.getElementById('insights-result');
const insightsContentDiv = document.getElementById('insights-content');
const insightsErrorDiv = document.getElementById('insights-error'); // Added error div
const insightsSpinner = document.getElementById('insights-spinner');
const generateInsightsBtn = document.getElementById('generate-insights-btn');
const selectedCountSpan = document.getElementById('selected-count');
const analysisRoleSpan = document.getElementById('analysis-role');
const selectAllBtn = document.getElementById('select-all');
const deselectAllBtn = document.getElementById('deselect-all');
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const collapseVizButton = document.getElementById('collapseVizButton');
const collapseVizIcon = collapseVizButton.querySelector('i');
const collapseVizText = collapseVizButton.querySelector('span');
const visualizationsCollapse = document.getElementById('visualizationsCollapse');
// NL Query Elements
const nlQueryInput = document.getElementById('nl-query-input');
const nlQuerySubmitBtn = document.getElementById('nl-query-submit');
const nlQuerySpinner = document.getElementById('nl-query-spinner');
const nlQueryErrorDiv = document.getElementById('nl-query-error');
// --- Chart Variables ---
const chartInstances = {}; // Store instances by ID
// --- Chart Options ---
const commonChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.7)', titleFont: { size: 14 }, bodyFont: { size: 12 }, padding: 10 } }, layout: { padding: { top: 5, bottom: 5 } } };
const verticalBarOptions = { ...commonChartOptions, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } };
const horizontalBarOptions = { ...commonChartOptions, indexAxis: 'y', scales: { x: { beginAtZero: true, ticks: { precision: 0 } } } };
const pieChartOptions = { ...commonChartOptions, plugins: { legend: { position: 'right', labels: { font: { size: 10 }, boxWidth: 10 } }, tooltip: commonChartOptions.plugins.tooltip } };
const chartColors = ['rgba(0, 123, 255, 0.7)', 'rgba(255, 193, 7, 0.7)', 'rgba(40, 167, 69, 0.7)', 'rgba(220, 53, 69, 0.7)', 'rgba(23, 162, 184, 0.7)', 'rgba(108, 117, 125, 0.7)', 'rgba(52, 58, 64, 0.7)', 'rgba(111, 66, 193, 0.7)', 'rgba(253, 126, 20, 0.7)', 'rgba(32, 201, 151, 0.7)'];
const chartBorderColors = chartColors.map(color => color.replace('0.7', '1'));
// --- State ---
let currentFilteredStudentsData = []; // Stores full data for visible students
// --- Utility Functions ---
const getSafe = (fn, defaultVal) => { try { const result = fn(); return (result === undefined || result === null) ? defaultVal : result; } catch (e) { return defaultVal; } };
function showCanvasMessage(canvasId, message) {
const ctx = document.getElementById(canvasId)?.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#aaa'; ctx.font = '14px Arial';
ctx.fillText(message, ctx.canvas.width / 2, ctx.canvas.height / 2); ctx.restore();
}
}
function createOrUpdateChart(canvasId, type, chartDataObj, options) {
const ctx = document.getElementById(canvasId)?.getContext('2d');
if (!ctx) { console.error(`Canvas element "${canvasId}" not found.`); return; }
if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); }
const labels = getSafe(() => chartDataObj.labels, []);
const dataValues = getSafe(() => chartDataObj.data, []);
const numericData = dataValues.map(Number);
if (labels.length === 0 || numericData.length === 0 || numericData.every(v => v === 0 && !isNaN(v))) {
showCanvasMessage(canvasId, 'No data available'); chartInstances[canvasId] = null; return;
}
try {
chartInstances[canvasId] = new Chart(ctx, { type: type, data: { labels: labels, datasets: [{ label: canvasId.replace('Chart', ''), data: numericData, backgroundColor: chartColors, borderColor: chartBorderColors, borderWidth: 1 }] }, options: options });
console.log(`Chart updated: ${canvasId}`);
} catch (error) {
console.error(`Error creating chart ${canvasId}:`, error); showCanvasMessage(canvasId, 'Error loading chart'); chartInstances[canvasId] = null;
}
}
function updateAllCharts(chartData) {
console.log("Updating All Charts (Frontend) with:", chartData);
if (!chartData) { console.error("updateAllCharts called with null/undefined data."); return; }
const defaultData = { labels: [], data: [] };
createOrUpdateChart('cgpaChart', 'bar', chartData.cgpa || defaultData, verticalBarOptions);
createOrUpdateChart('deptChart', 'pie', chartData.department || defaultData, pieChartOptions);
createOrUpdateChart('skillsChart', 'bar', chartData.skills || defaultData, horizontalBarOptions);
createOrUpdateChart('rolesChart', 'bar', chartData.roles || defaultData, horizontalBarOptions);
}
function updateStudentsTable(students) {
console.log(`Updating table with ${students ? students.length : 0} students.`);
studentsTableBody.innerHTML = '';
currentFilteredStudentsData = students || []; // Store full data for export/AI
if (!currentFilteredStudentsData || currentFilteredStudentsData.length === 0) {
noStudentsMessage.classList.remove('d-none');
exportCsvBtn.disabled = true;
} else {
noStudentsMessage.classList.add('d-none');
exportCsvBtn.disabled = false;
currentFilteredStudentsData.forEach(student => {
const row = document.createElement('tr');
row.dataset.email = student.email;
const fullName = getSafe(() => student.full_name, 'N/A');
const department = getSafe(() => student.department, 'N/A');
const cgpa = getSafe(() => student.cgpa, null);
const cgpaFormatted = cgpa !== null ? cgpa.toFixed(2) : 'N/A';
const backlogs = getSafe(() => student.backlogs, 0);
// Simplified Skill Display Logic (handles list or string)
let skillsRaw = getSafe(() => student.programming_languages, []);
let skillsArray = [];
if (typeof skillsRaw === 'string') {
skillsArray = skillsRaw.split(',').map(s => s.trim()).filter(Boolean);
} else if (Array.isArray(skillsRaw)) {
skillsArray = skillsRaw.map(s => String(s).trim()).filter(Boolean);
}
// Remove proficiency in brackets for display
const skillsCleaned = skillsArray.map(s => s.replace(/\s*\(.*\)$/, ''));
const topSkills = skillsCleaned.slice(0, 3).join(', ') + (skillsCleaned.length > 3 ? '...' : '');
let experienceHtml = '';
if (getSafe(() => student.internships, null)) experienceHtml += '<span class="badge bg-success" title="Internship">I</span> ';
if (getSafe(() => student.projects, null)) experienceHtml += '<span class="badge bg-primary" title="Projects">P</span> ';
if (getSafe(() => student.hackathons, null)) experienceHtml += '<span class="badge bg-warning text-dark" title="Hackathon">H</span> ';
if (getSafe(() => student.publications, null)) experienceHtml += '<span class="badge bg-info text-dark" title="Publication/Research">R</span> ';
if (!experienceHtml) experienceHtml = '<span class="text-muted small">None</span>';
row.innerHTML = `
<td class="text-center"><input type="checkbox" class="form-check-input student-checkbox" value="${student.email}"></td>
<td>${fullName}</td>
<td>${department}</td>
<td>${cgpaFormatted}</td>
<td>${backlogs}</td>
<td><small>${topSkills || 'N/A'}</small></td>
<td>${experienceHtml.trim()}</td>
`;
studentsTableBody.appendChild(row);
});
}
studentCountBadge.textContent = currentFilteredStudentsData.length;
selectAllCheckbox.checked = false;
updateAiButtonState();
}
function updateAiButtonState() {
const selectedCheckboxes = studentsTableBody.querySelectorAll('.student-checkbox:checked');
const count = selectedCheckboxes.length;
selectedCountSpan.textContent = count;
generateInsightsBtn.disabled = count === 0;
}
function exportToCsv(filename, rows) {
console.log(`Exporting ${rows.length} rows to ${filename}`);
if (rows.length === 0) { alert("No data to export."); return; }
// Use the actual headers from the first row object keys for flexibility
const headers = Object.keys(rows[0]);
const processRow = (row) => headers.map(header => {
let value = row[header]; if (value === null || value === undefined) return ''; if (Array.isArray(value)) value = value.join('; '); let stringValue = String(value); if (stringValue.includes('"') || stringValue.includes(',') || stringValue.includes('\n')) stringValue = `"${stringValue.replace(/"/g, '""')}"`; return stringValue;
}).join(',');
const csvContent = [headers.join(','), ...rows.map(processRow)].join('\n');
const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' }); // Add BOM
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); URL.revokeObjectURL(url); } else { alert("CSV export is not supported."); }
}
// --- Function to update filter form from AI results ---
function updateFilterForm(filters) {
console.log("Updating filter form with:", filters);
// Resetting form ensures clean slate
filterForm.reset();
// Reset multi-selects explicitly
filterForm.querySelectorAll('select[multiple]').forEach(select => {
Array.from(select.options).forEach(option => option.selected = false);
});
// Clear Job ID filter if applying manual/AI filters
document.getElementById('job_id_filter').value = '';
// Apply filters from AI
if (filters.min_cgpa !== undefined && filters.min_cgpa !== null) {
document.getElementById('min_cgpa_filter').value = filters.min_cgpa;
} else {
document.getElementById('min_cgpa_filter').value = ''; // Explicitly clear if null/undefined
}
if (filters.max_backlogs !== undefined && filters.max_backlogs !== null) {
document.getElementById('max_backlogs_filter').value = filters.max_backlogs;
} else {
document.getElementById('max_backlogs_filter').value = ''; // Explicitly clear
}
// Boolean flags
document.getElementById('hasHackathons').checked = filters.has_hackathons === true;
document.getElementById('hasExperience').checked = filters.has_experience === true;
document.getElementById('hasResearch').checked = filters.has_research === true;
// Handle multi-selects (Skills, Roles, Certifications)
function applyMultiSelect(selectId, values) {
if (values && Array.isArray(values) && values.length > 0) {
const selectElement = document.getElementById(selectId);
if (selectElement) {
Array.from(selectElement.options).forEach(option => {
option.selected = values.some(val =>
option.value.toLowerCase() === String(val).toLowerCase().trim() ||
option.text.toLowerCase() === String(val).toLowerCase().trim()
);
});
} else {
console.warn(`Select element #${selectId} not found.`);
}
}
}
applyMultiSelect('skills_filter', filters.skills);
applyMultiSelect('roles_filter', filters.roles);
applyMultiSelect('certifications_filter', filters.certifications);
// Handle department (if mentioned by AI and a filter field exists)
// Note: 'department' is not currently a standard filter field in the form
if (filters.department) {
console.log(`AI suggested department filter: ${filters.department} (No dedicated form field yet)`);
// If you add a department filter input/select later, update it here:
// const deptSelect = document.getElementById('department_filter');
// if (deptSelect) deptSelect.value = filters.department;
}
console.log("Filter form updated by AI result.");
}
// --- Event Listeners ---
filterForm.addEventListener('submit', function(e) {
e.preventDefault(); const formData = new FormData(filterForm);
console.log("Applying Filters (Manual or AI Triggered)...");
filterForm.querySelector('button[type="submit"]').disabled = true;
filterForm.querySelector('button[type="submit"]').innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Filtering...';
noStudentsMessage.classList.add('d-none'); // Hide message initially
// Clear Job ID if manual filters are being changed (except job ID itself)
const jobIdFilter = document.getElementById('job_id_filter');
if (e.submitter !== jobIdFilter && jobIdFilter.value !== '') {
// Check if the submit was triggered by something other than changing the job filter itself
// AND if a job ID was previously selected. This logic might need refinement depending on desired interaction.
// A simpler approach might be to always clear job ID if any manual filter is touched.
// console.log("Manual filter change detected, clearing Job ID filter.");
// jobIdFilter.value = '';
// formData.set('job_id', ''); // Update formData as well
}
fetch('{{ url_for("filter_students_route") }}', { method: 'POST', body: formData })
.then(response => { if (!response.ok) { return response.json().then(errData => { throw new Error(getSafe(() => errData.error, `HTTP ${response.status}`)); }).catch(() => { throw new Error(`HTTP ${response.status}`); }); } return response.json(); })
.then(data => {
console.log("Filter Response:", data);
if (data.error) { throw new Error(data.error); }
updateStudentsTable(data.students); // Update table with full data
if (data.chart_data) {
updateAllCharts(data.chart_data);
} else { console.error("Chart data missing in filter response!"); }
exportCsvBtn.disabled = data.students.length === 0;
})
.catch(error => {
console.error('Filter Error:', error);
noStudentsMessage.textContent = `Error applying filters: ${error.message}`;
noStudentsMessage.classList.remove('d-none', 'alert-info');
noStudentsMessage.classList.add('alert-danger');
studentsTableBody.innerHTML = ''; // Clear table on error
studentCountBadge.textContent = 0;
updateAllCharts({ cgpa: {}, department: {}, skills: {}, roles: {} }); // Clear charts
exportCsvBtn.disabled = true;
})
.finally(() => {
filterForm.querySelector('button[type="submit"]').disabled = false;
filterForm.querySelector('button[type="submit"]').innerHTML = '<i class="bi bi-funnel-fill"></i> Apply Filters';
});
});
resetButton.addEventListener('click', function() {
console.log("Resetting filters...");
filterForm.reset();
// Explicitly reset multi-selects
filterForm.querySelectorAll('select[multiple]').forEach(select => {
Array.from(select.options).forEach(option => option.selected = false);
});
// Reset Job ID filter
document.getElementById('job_id_filter').value = '';
// Automatically trigger a filter update after reset
setTimeout(() => filterForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })), 50);
});
exportCsvBtn.addEventListener('click', () => {
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, "");
const filename = `filtered_students_${timestamp}.csv`;
// Export the stored full data of currently filtered students
exportToCsv(filename, currentFilteredStudentsData);
});
selectAllBtn.addEventListener('click', () => { studentsTableBody.querySelectorAll('.student-checkbox:not(:disabled)').forEach(cb => cb.checked = true); selectAllCheckbox.checked = true; updateAiButtonState(); });
deselectAllBtn.addEventListener('click', () => { studentsTableBody.querySelectorAll('.student-checkbox:not(:disabled)').forEach(cb => cb.checked = false); selectAllCheckbox.checked = false; updateAiButtonState(); });
selectAllCheckbox.addEventListener('change', function() { studentsTableBody.querySelectorAll('.student-checkbox:not(:disabled)').forEach(cb => cb.checked = this.checked); updateAiButtonState(); });
studentsTableBody.addEventListener('change', function(event) {
if (event.target.classList.contains('student-checkbox')) {
updateAiButtonState();
const allVisible = studentsTableBody.querySelectorAll('.student-checkbox:not(:disabled)');
const checkedVisible = studentsTableBody.querySelectorAll('.student-checkbox:checked:not(:disabled)');
selectAllCheckbox.checked = allVisible.length > 0 && allVisible.length === checkedVisible.length;
}
});
aiInsightsForm.addEventListener('submit', function(e) {
e.preventDefault();
const selectedStudents = Array.from(studentsTableBody.querySelectorAll('.student-checkbox:checked')).map(cb => cb.value);
const role = aiRoleInput.value.trim();
if (selectedStudents.length === 0) { alert('Please select one or more students from the table first.'); return; }
if (!role) { alert('Please enter the Job Role for analysis.'); aiRoleInput.focus(); return; }
const formData = new FormData();
selectedStudents.forEach(email => formData.append('selected_students', email));
formData.append('role', role);
insightsResultDiv.classList.remove('d-none');
insightsContentDiv.innerHTML = '';
insightsErrorDiv.classList.add('d-none'); // Hide previous errors
insightsSpinner.classList.remove('d-none');
generateInsightsBtn.disabled = true;
generateInsightsBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Generating...';
analysisRoleSpan.textContent = `(for ${role})`;
fetch('{{ url_for("ai_insights_route") }}', { method: 'POST', body: formData })
.then(response => { if (!response.ok) { return response.json().then(errData => { throw new Error(getSafe(() => errData.error, `HTTP ${response.status}`)); }).catch(() => { throw new Error(`HTTP ${response.status}`); }); } return response.json(); })
.then(data => {
console.log("AI Insights Response:", data);
if (data.error) {
throw new Error(data.error); // Throw error to be caught below
} else {
let formatted = data.insights || "No insights generated.";
// Basic Markdown-like formatting
formatted = formatted
.replace(/</g, "<").replace(/>/g, ">") // Basic HTML escaping first
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Bold
.replace(/\*(.*?)\*/g, '<em>$1</em>') // Italics
.replace(/^### (.*$)/gim, '<h6 class="mt-2 mb-1">$1</h6>') // Headings
.replace(/^## (.*$)/gim, '<h5 class="mt-2 mb-1">$1</h5>')
.replace(/^# (.*$)/gim, '<h4 class="mt-2 mb-1">$1</h4>')
.replace(/^\s*[-*+] (.*$)/gim, '<li>$1</li>'); // List items
// Wrap consecutive list items in <ul>
formatted = formatted.replace(/(<li>.*<\/li>\s*)+/g, (match) => `<ul class="list-unstyled mb-2 ms-3">${match.replace(/<\/li>\s*<li>/g, '</li><li>')}</ul>`);
// Replace newlines with <br>, but not inside lists or after headings
formatted = formatted.replace(/(\<\/(?:ul|h[4-6])\>)\n+/g, '$1'); // Remove newlines after block elements
formatted = formatted.replace(/\n/g, '<br>'); // Convert remaining newlines
insightsContentDiv.innerHTML = formatted;
insightsErrorDiv.classList.add('d-none');
}
})
.catch(error => {
console.error('AI Insights Error:', error);
insightsErrorDiv.textContent = `Error: ${error.message}`;
insightsErrorDiv.classList.remove('d-none');
insightsContentDiv.innerHTML = ''; // Clear content on error
})
.finally(() => {
insightsSpinner.classList.add('d-none');
// Re-enable button using the count from the function
updateAiButtonState();
const currentCount = studentsTableBody.querySelectorAll('.student-checkbox:checked').length;
generateInsightsBtn.innerHTML = `<i class="bi bi-magic"></i> Generate Insights for Selected (${currentCount})`;
});
});
// Event Listener for Natural Language Query
nlQuerySubmitBtn.addEventListener('click', function() {
const query = nlQueryInput.value.trim();
if (!query) {
nlQueryErrorDiv.textContent = 'Please enter a query.';
nlQueryErrorDiv.classList.remove('d-none');
return;
}
nlQuerySubmitBtn.disabled = true;
nlQuerySpinner.classList.remove('d-none');
nlQueryErrorDiv.classList.add('d-none');
nlQueryErrorDiv.textContent = '';
fetch('{{ url_for("process_nl_query") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query })
})
.then(response => {
if (!response.ok) {
return response.json().then(errData => { throw new Error(errData.error || `HTTP error! Status: ${response.status}`); })
.catch(() => { throw new Error(`HTTP error! Status: ${response.status}`); });
}
return response.json();
})
.then(data => {
if (data.error) { throw new Error(data.error); }
console.log("Received filter parameters from AI:", data.filters);
updateFilterForm(data.filters);
console.log("Triggering filter form submission...");
// Use setTimeout to allow the browser to update the form values before submitting
setTimeout(() => {
filterForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}, 50); // Small delay
})
.catch(error => {
console.error('Error processing NL query:', error);
nlQueryErrorDiv.textContent = `Error translating query: ${error.message}`;
nlQueryErrorDiv.classList.remove('d-none');
})
.finally(() => {
nlQuerySubmitBtn.disabled = false;
nlQuerySpinner.classList.add('d-none');
});
});
visualizationsCollapse.addEventListener('show.bs.collapse', () => { collapseVizIcon.classList.replace('bi-chevron-down', 'bi-chevron-up'); collapseVizText.textContent = 'Collapse'; });
visualizationsCollapse.addEventListener('hide.bs.collapse', () => { collapseVizIcon.classList.replace('bi-chevron-up', 'bi-chevron-down'); collapseVizText.textContent = 'Expand'; });
// --- Initial Load ---
console.log("--- Initializing Dashboard ---");
// Data is rendered server-side initially. JS updates state.
currentFilteredStudentsData = allStudentsInitially; // Initialize JS state
updateAiButtonState(); // Set initial AI button state
exportCsvBtn.disabled = currentFilteredStudentsData.length === 0; // Set initial export button state
if (initialChartData) {
console.log("Initializing Charts with Initial Data (Frontend):", initialChartData);
updateAllCharts(initialChartData);
} else {
console.error("Initial chart data is missing!");
updateAllCharts({ cgpa: {}, department: {}, skills: {}, roles: {} }); // Init empty charts
}
console.log("--- Dashboard Initialized ---");
}); // End DOMContentLoaded
</script>
</body>
</html>