Initial commit: AI Recruitment Site for Ryans Recruit Firm
- Complete PostgreSQL schema with migrations - Node.js/Express backend with authentication - Public website (home, about, services, jobs, apply, contact) - Admin dashboard with applicant and job management - CV upload and storage in PostgreSQL BYTEA - Docker Compose setup for deployment - Session-based authentication - Responsive design with Ryan brand colors
This commit is contained in:
290
public/js/admin.js
Normal file
290
public/js/admin.js
Normal file
@@ -0,0 +1,290 @@
|
||||
// Admin JavaScript for Ryans Recruit Firm
|
||||
|
||||
// Check authentication on all admin pages except login
|
||||
if (!window.location.pathname.includes('login.html')) {
|
||||
checkAuth();
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/check');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.loggedIn) {
|
||||
window.location.href = '/admin/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update admin name in header if exists
|
||||
const adminNameEl = document.getElementById('admin-name');
|
||||
if (adminNameEl && data.admin) {
|
||||
adminNameEl.textContent = data.admin.fullName;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Login page handlers
|
||||
if (window.location.pathname.includes('login.html')) {
|
||||
checkFirstAdmin();
|
||||
document.getElementById('login-form')?.addEventListener('submit', handleLogin);
|
||||
}
|
||||
|
||||
async function checkFirstAdmin() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/check-first');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.isFirstAdmin) {
|
||||
document.getElementById('first-admin-notice').style.display = 'block';
|
||||
document.getElementById('full-name-group').style.display = 'block';
|
||||
document.querySelector('button[type="submit"]').textContent = 'Create Admin Account';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking first admin:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Processing...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Login successful!', 'success');
|
||||
setTimeout(() => window.location.href = '/admin/dashboard.html', 500);
|
||||
} else {
|
||||
showAlert(result.error || 'Login failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
showAlert('Login failed. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard page
|
||||
if (window.location.pathname.includes('dashboard.html')) {
|
||||
loadDashboardStats();
|
||||
}
|
||||
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/stats');
|
||||
const stats = await response.json();
|
||||
|
||||
document.getElementById('stat-new-applications').textContent = stats.new_applications || 0;
|
||||
document.getElementById('stat-total-applications').textContent = stats.total_applications || 0;
|
||||
document.getElementById('stat-total-applicants').textContent = stats.total_applicants || 0;
|
||||
document.getElementById('stat-active-jobs').textContent = stats.active_jobs || 0;
|
||||
|
||||
if (stats.unread_messages > 0) {
|
||||
document.getElementById('stat-unread-messages').textContent = stats.unread_messages;
|
||||
document.getElementById('stat-unread-messages').parentElement.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading stats:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Applications page
|
||||
if (window.location.pathname.includes('applicants.html')) {
|
||||
loadApplications();
|
||||
}
|
||||
|
||||
async function loadApplications(filters = {}) {
|
||||
const container = document.getElementById('applications-container');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams(filters);
|
||||
const response = await fetch(`/api/admin/applications?${params}`);
|
||||
const applications = await response.json();
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (applications.length === 0) {
|
||||
container.innerHTML = '<div class="text-center"><p>No applications found.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Applicant</th>
|
||||
<th>Job</th>
|
||||
<th>Experience</th>
|
||||
<th>Applied</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${applications.map(app => `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${escapeHtml(app.full_name)}</strong><br>
|
||||
<small>${escapeHtml(app.email)}</small>
|
||||
</td>
|
||||
<td>${escapeHtml(app.job_title || 'General Application')}</td>
|
||||
<td>${app.years_of_experience || 'N/A'} years</td>
|
||||
<td>${new Date(app.applied_at).toLocaleDateString()}</td>
|
||||
<td><span class="tag tag-${getStatusColor(app.status)}">${escapeHtml(app.status)}</span></td>
|
||||
<td>
|
||||
<a href="/admin/applicants.html?id=${app.id}" class="btn btn-sm btn-primary">View</a>
|
||||
<a href="/api/admin/applications/${app.id}/cv" class="btn btn-sm btn-secondary" target="_blank">CV</a>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading applications:', err);
|
||||
if (loading) loading.style.display = 'none';
|
||||
container.innerHTML = '<div class="alert alert-error">Failed to load applications</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs management page
|
||||
if (window.location.pathname.includes('jobs.html') && window.location.pathname.includes('admin')) {
|
||||
loadAdminJobs();
|
||||
}
|
||||
|
||||
async function loadAdminJobs() {
|
||||
const container = document.getElementById('jobs-container');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/jobs');
|
||||
const jobs = await response.json();
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<button onclick="showJobForm()" class="btn btn-success">+ Create New Job</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Department</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${jobs.map(job => `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(job.title)}</strong></td>
|
||||
<td>${escapeHtml(job.department || 'N/A')}</td>
|
||||
<td>${escapeHtml(job.location || 'N/A')}</td>
|
||||
<td><span class="tag ${job.is_active ? 'tag-success' : 'tag-error'}">${job.is_active ? 'Active' : 'Inactive'}</span></td>
|
||||
<td>${new Date(job.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<button onclick="editJob(${job.id})" class="btn btn-sm btn-primary">Edit</button>
|
||||
<button onclick="toggleJobStatus(${job.id}, ${!job.is_active})" class="btn btn-sm btn-secondary">${job.is_active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading jobs:', err);
|
||||
if (loading) loading.style.display = 'none';
|
||||
container.innerHTML = '<div class="alert alert-error">Failed to load jobs</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleJobStatus(jobId, isActive) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/jobs/${jobId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isActive })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`Job ${isActive ? 'activated' : 'deactivated'} successfully`, 'success');
|
||||
loadAdminJobs();
|
||||
} else {
|
||||
showAlert('Failed to update job status', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating job:', err);
|
||||
showAlert('Failed to update job status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/api/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login.html';
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'new': 'primary',
|
||||
'reviewing': 'warning',
|
||||
'interview': 'info',
|
||||
'hired': 'success',
|
||||
'rejected': 'error'
|
||||
};
|
||||
return colors[status] || 'primary';
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type}`;
|
||||
alertDiv.textContent = message;
|
||||
alertDiv.style.position = 'fixed';
|
||||
alertDiv.style.top = '20px';
|
||||
alertDiv.style.right = '20px';
|
||||
alertDiv.style.zIndex = '10000';
|
||||
alertDiv.style.minWidth = '300px';
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
Reference in New Issue
Block a user