Initial commit
This commit is contained in:
		
							
								
								
									
										478
									
								
								backend/src/routes/jobs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										478
									
								
								backend/src/routes/jobs.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,478 @@ | ||||
| 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; | ||||
		Reference in New Issue
	
	Block a user