Files
MOHPortal/backend/src/routes/jobs.js
2025-10-16 17:04:52 -05:00

479 lines
14 KiB
JavaScript

const express = require('express');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Get all jobs with filtering and search
router.get('/', authenticateToken, async (req, res) => {
try {
const {
search,
location,
employmentType,
experienceLevel,
skills,
salaryMin,
salaryMax,
remoteAllowed,
status = 'active',
page = 1,
limit = 10
} = req.query;
let query = `
SELECT j.*, e.company_name, e.industry, e.company_size
FROM jobs j
JOIN employers e ON j.employer_id = e.id
`;
const queryParams = [];
let paramCount = 0;
const conditions = [];
// Always filter by status
paramCount++;
conditions.push(`j.status = $${paramCount}`);
queryParams.push(status);
if (search) {
paramCount++;
conditions.push(`(j.title ILIKE $${paramCount} OR j.description ILIKE $${paramCount})`);
queryParams.push(`%${search}%`);
}
if (location) {
paramCount++;
conditions.push(`j.location ILIKE $${paramCount}`);
queryParams.push(`%${location}%`);
}
if (employmentType) {
paramCount++;
conditions.push(`j.employment_type = $${paramCount}`);
queryParams.push(employmentType);
}
if (experienceLevel) {
paramCount++;
conditions.push(`j.experience_level = $${paramCount}`);
queryParams.push(experienceLevel);
}
if (skills) {
const skillArray = skills.split(',').map(s => s.trim());
paramCount++;
conditions.push(`j.skills_required && $${paramCount}`);
queryParams.push(skillArray);
}
if (salaryMin) {
paramCount++;
conditions.push(`j.salary_max >= $${paramCount}`);
queryParams.push(parseInt(salaryMin));
}
if (salaryMax) {
paramCount++;
conditions.push(`j.salary_min <= $${paramCount}`);
queryParams.push(parseInt(salaryMax));
}
if (remoteAllowed === 'true') {
conditions.push(`j.remote_allowed = true`);
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`;
}
query += ` ORDER BY j.created_at DESC`;
// Add pagination
const offset = (page - 1) * limit;
paramCount++;
query += ` LIMIT $${paramCount}`;
queryParams.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
queryParams.push(offset);
const result = await pool.query(query, queryParams);
// Get total count for pagination
let countQuery = `
SELECT COUNT(*)
FROM jobs j
JOIN employers e ON j.employer_id = e.id
`;
const countParams = [];
let countParamCount = 0;
const countConditions = [];
countParamCount++;
countConditions.push(`j.status = $${countParamCount}`);
countParams.push(status);
if (search) {
countParamCount++;
countConditions.push(`(j.title ILIKE $${countParamCount} OR j.description ILIKE $${countParamCount})`);
countParams.push(`%${search}%`);
}
if (location) {
countParamCount++;
countConditions.push(`j.location ILIKE $${countParamCount}`);
countParams.push(`%${location}%`);
}
if (employmentType) {
countParamCount++;
countConditions.push(`j.employment_type = $${countParamCount}`);
countParams.push(employmentType);
}
if (experienceLevel) {
countParamCount++;
countConditions.push(`j.experience_level = $${countParamCount}`);
countParams.push(experienceLevel);
}
if (skills) {
const skillArray = skills.split(',').map(s => s.trim());
countParamCount++;
countConditions.push(`j.skills_required && $${countParamCount}`);
countParams.push(skillArray);
}
if (salaryMin) {
countParamCount++;
countConditions.push(`j.salary_max >= $${countParamCount}`);
countParams.push(parseInt(salaryMin));
}
if (salaryMax) {
countParamCount++;
countConditions.push(`j.salary_min <= $${countParamCount}`);
countParams.push(parseInt(salaryMax));
}
if (remoteAllowed === 'true') {
countConditions.push(`j.remote_allowed = true`);
}
if (countConditions.length > 0) {
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
}
const countResult = await pool.query(countQuery, countParams);
res.json({
jobs: result.rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(countResult.rows[0].count),
pages: Math.ceil(countResult.rows[0].count / limit)
}
});
} catch (error) {
console.error('Get jobs error:', error);
res.status(500).json({ error: 'Failed to fetch jobs' });
}
});
// Get job by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT j.*, e.company_name, e.industry, e.company_size, e.website, e.description as company_description
FROM jobs j
JOIN employers e ON j.employer_id = e.id
WHERE j.id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Get job error:', error);
res.status(500).json({ error: 'Failed to fetch job' });
}
});
// Create job posting
router.post('/', authenticateToken, requireRole(['employer', 'recruiter']), [
body('title').notEmpty().trim(),
body('description').notEmpty().trim(),
body('requirements').isArray(),
body('responsibilities').isArray(),
body('location').notEmpty().trim(),
body('employmentType').isIn(['full-time', 'part-time', 'contract', 'internship']),
body('salaryMin').optional().isInt({ min: 0 }),
body('salaryMax').optional().isInt({ min: 0 }),
body('currency').optional().isLength({ min: 3, max: 3 }),
body('remoteAllowed').optional().isBoolean(),
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
body('skillsRequired').optional().isArray(),
body('benefits').optional().isArray(),
body('applicationDeadline').optional().isISO8601()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
title,
description,
requirements,
responsibilities,
location,
employmentType,
salaryMin,
salaryMax,
currency = 'USD',
remoteAllowed = false,
experienceLevel,
skillsRequired,
benefits,
applicationDeadline
} = req.body;
// Get employer_id for the current user
let employerId;
if (req.user.role === 'employer') {
const employerResult = await pool.query(
'SELECT id FROM employers WHERE user_id = $1',
[req.user.id]
);
if (employerResult.rows.length === 0) {
return res.status(400).json({ error: 'Employer profile not found' });
}
employerId = employerResult.rows[0].id;
} else {
// For recruiters, they need to specify which employer
const { employerId: providedEmployerId } = req.body;
if (!providedEmployerId) {
return res.status(400).json({ error: 'Employer ID required for recruiters' });
}
employerId = providedEmployerId;
}
const result = await pool.query(`
INSERT INTO jobs (employer_id, title, description, requirements, responsibilities, location, employment_type, salary_min, salary_max, currency, remote_allowed, experience_level, skills_required, benefits, application_deadline)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING *
`, [employerId, title, description, requirements, responsibilities, location, employmentType, salaryMin, salaryMax, currency, remoteAllowed, experienceLevel, skillsRequired, benefits, applicationDeadline]);
res.status(201).json({
message: 'Job posting created successfully',
job: result.rows[0]
});
} catch (error) {
console.error('Create job error:', error);
res.status(500).json({ error: 'Failed to create job posting' });
}
});
// Update job posting
router.put('/:id', authenticateToken, [
body('title').optional().notEmpty().trim(),
body('description').optional().notEmpty().trim(),
body('requirements').optional().isArray(),
body('responsibilities').optional().isArray(),
body('location').optional().notEmpty().trim(),
body('employmentType').optional().isIn(['full-time', 'part-time', 'contract', 'internship']),
body('salaryMin').optional().isInt({ min: 0 }),
body('salaryMax').optional().isInt({ min: 0 }),
body('currency').optional().isLength({ min: 3, max: 3 }),
body('status').optional().isIn(['active', 'paused', 'closed', 'draft']),
body('remoteAllowed').optional().isBoolean(),
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
body('skillsRequired').optional().isArray(),
body('benefits').optional().isArray(),
body('applicationDeadline').optional().isISO8601()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const {
title,
description,
requirements,
responsibilities,
location,
employmentType,
salaryMin,
salaryMax,
currency,
status,
remoteAllowed,
experienceLevel,
skillsRequired,
benefits,
applicationDeadline
} = req.body;
// Check if job exists and user has permission
const jobResult = await pool.query(`
SELECT j.*, e.user_id as employer_user_id
FROM jobs j
JOIN employers e ON j.employer_id = e.id
WHERE j.id = $1
`, [id]);
if (jobResult.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
const job = jobResult.rows[0];
// Users can only update jobs from their own employer unless they're admin
if (job.employer_user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (title) {
updateFields.push(`title = $${paramCount}`);
updateValues.push(title);
paramCount++;
}
if (description) {
updateFields.push(`description = $${paramCount}`);
updateValues.push(description);
paramCount++;
}
if (requirements) {
updateFields.push(`requirements = $${paramCount}`);
updateValues.push(requirements);
paramCount++;
}
if (responsibilities) {
updateFields.push(`responsibilities = $${paramCount}`);
updateValues.push(responsibilities);
paramCount++;
}
if (location) {
updateFields.push(`location = $${paramCount}`);
updateValues.push(location);
paramCount++;
}
if (employmentType) {
updateFields.push(`employment_type = $${paramCount}`);
updateValues.push(employmentType);
paramCount++;
}
if (salaryMin !== undefined) {
updateFields.push(`salary_min = $${paramCount}`);
updateValues.push(salaryMin);
paramCount++;
}
if (salaryMax !== undefined) {
updateFields.push(`salary_max = $${paramCount}`);
updateValues.push(salaryMax);
paramCount++;
}
if (currency) {
updateFields.push(`currency = $${paramCount}`);
updateValues.push(currency);
paramCount++;
}
if (status) {
updateFields.push(`status = $${paramCount}`);
updateValues.push(status);
paramCount++;
}
if (remoteAllowed !== undefined) {
updateFields.push(`remote_allowed = $${paramCount}`);
updateValues.push(remoteAllowed);
paramCount++;
}
if (experienceLevel) {
updateFields.push(`experience_level = $${paramCount}`);
updateValues.push(experienceLevel);
paramCount++;
}
if (skillsRequired) {
updateFields.push(`skills_required = $${paramCount}`);
updateValues.push(skillsRequired);
paramCount++;
}
if (benefits) {
updateFields.push(`benefits = $${paramCount}`);
updateValues.push(benefits);
paramCount++;
}
if (applicationDeadline) {
updateFields.push(`application_deadline = $${paramCount}`);
updateValues.push(applicationDeadline);
paramCount++;
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'No fields to update' });
}
updateValues.push(id);
const query = `UPDATE jobs SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
const result = await pool.query(query, updateValues);
res.json({
message: 'Job posting updated successfully',
job: result.rows[0]
});
} catch (error) {
console.error('Update job error:', error);
res.status(500).json({ error: 'Failed to update job posting' });
}
});
// Delete job posting
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
// Check if job exists and user has permission
const jobResult = await pool.query(`
SELECT j.*, e.user_id as employer_user_id
FROM jobs j
JOIN employers e ON j.employer_id = e.id
WHERE j.id = $1
`, [id]);
if (jobResult.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
const job = jobResult.rows[0];
// Users can only delete jobs from their own employer unless they're admin
if (job.employer_user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
await pool.query('DELETE FROM jobs WHERE id = $1', [id]);
res.json({ message: 'Job posting deleted successfully' });
} catch (error) {
console.error('Delete job error:', error);
res.status(500).json({ error: 'Failed to delete job posting' });
}
});
module.exports = router;