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;