479 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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;
 |