Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,779 changes: 1,772 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions src/controllers/collaborationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,110 @@ exports.reorderQuestions = async (req, res) => {
res.status(500).json({ message: 'Error reordering questions.', error: error.message });
}
};

/**
* POST /api/jobforms/:formId/responses
* Submit an application response to a job form.
*
* Body (JSON or multipart/form-data):
* respondent {string} - applicant full name (required)
* email {string} - applicant email (required)
* answers {Array} - [{ questionId, answer }]
*
* Optional multipart field:
* resume {file} - resume file (stored in Azure, URL saved on response)
*
* Responses:
* 201 - { message, response }
* 400 - missing required fields
* 409 - duplicate application
* 404 - form not found
* 500 - server error
*/
exports.submitFormResponse = async (req, res) => {
try {
const { formId } = req.params;
const { respondent, email, answers } = req.body;

// --- 400: validate required fields ---
if (!respondent || !email || !answers) {
return res.status(400).json({ error: 'respondent, email, and answers are required.' });
}

// --- 404: form must exist ---
const form = await Form.findById(formId);
if (!form) {
return res.status(404).json({ error: 'Form not found.' });
}

// --- 409: duplicate check (same email for same form) ---
const existing = await Response.findOne({ formId, email: email.trim().toLowerCase() });
if (existing) {
return res.status(409).json({ error: 'Application already submitted.' });
}

// --- Optional: resume upload to Azure ---
let resumeUrl = '';
if (req.file) {
try {
const { uploadFileToAzureBlobStorage } = require('../utilities/AzureBlobImages');
const safeFormTitle = form.title.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
const safeEmail = email.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
const ext = req.file.originalname.split('.').pop();
const blobName = `resumes/${safeFormTitle}_${safeEmail}_${Date.now()}.${ext}`;
resumeUrl = await uploadFileToAzureBlobStorage(req.file, blobName);
} catch (uploadErr) {
console.error('Resume upload failed (non-fatal):', uploadErr.message);
// Upload failure is non-fatal — proceed without resume
}
}

// --- Parse answers if sent as a JSON string (multipart/form-data case) ---
let parsedAnswers = answers;
if (typeof answers === 'string') {
try {
parsedAnswers = JSON.parse(answers);
} catch {
return res.status(400).json({ error: 'answers must be a valid JSON array.' });
}
}

// --- Save response ---
const response = new Response({
formId,
respondent: respondent.trim(),
email: email.trim().toLowerCase(),
answers: parsedAnswers,
resumeUrl,
});

await response.save();

// --- Send confirmation email (non-fatal if it fails) ---
try {
const emailSender = require('../utilities/emailSender');
const emailBody = `
<div style="font-family: Arial, sans-serif; padding: 20px; max-width: 600px;">
<h2>Application Received — ${form.title}</h2>
<p>Hi ${respondent.trim()},</p>
<p>Thank you for applying for <strong>${form.title}</strong>. We have received your application and will be in touch shortly.</p>
<p>If you have any questions, feel free to reach out.</p>
<br/>
<p>Best regards,<br/>One Community</p>
</div>
`;
await emailSender(
[email.trim().toLowerCase()],
`Application Received — ${form.title}`,
emailBody,
);
} catch (emailErr) {
console.error('Confirmation email failed (non-fatal):', emailErr.message);
}

return res.status(201).json({ message: 'Application submitted successfully.', response });
} catch (error) {
console.error('Error submitting form response:', error);
return res.status(500).json({ message: 'Error submitting application.', error: error.message });
}
};
16 changes: 16 additions & 0 deletions src/controllers/jobsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@ const getPositions = async (req, res) => {
}
};

/**
* GET /api/jobs/positions
* Returns a distinct list of active job position titles from the jobs collection.
* Used by the job application form dropdown.
*/
const getActiveJobPositions = async (req, res) => {
try {
const positions = await Job.distinct('title', {});
positions.sort((a, b) => a.localeCompare(b));
res.status(200).json({ positions });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch active job positions', details: error.message });
}
};

const getJobById = async (req, res) => {
const { id } = req.params;
try {
Expand Down Expand Up @@ -288,4 +303,5 @@ module.exports = {
getCategories,
reorderJobs,
getPositions,
getActiveJobPositions,
};
100 changes: 98 additions & 2 deletions src/controllers/prAnalytics/prGradingConfigController.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,100 @@
const prGradingConfigController = function (PRGradingConfig) {
const prGradingConfigController = function (PRGradingConfig, UserProfile, HgnFormResponse, Team) {
// Maps weeklycommittedHours to required PR count
const getPrsNeeded = (weeklyHours) => {
if (weeklyHours >= 35) return 30;
if (weeklyHours >= 26) return 20;
if (weeklyHours >= 15) return 10;
if (weeklyHours >= 10) return 7;
return 0;
};

// Syncs reviewerNames in prGradingConfigs based on:
// - Users who have submitted the Skills Questionnaire (hgnFormResponses)
// - Excluding users already on a dev team (teams.members)
// - Split A-M -> Team 1, N-Z -> Team 2
const syncReviewers = async (req, res) => {
try {
// Step 1: Get all user_ids from hgnFormResponses
const formResponses = await HgnFormResponse.find(
{ user_id: { $exists: true, $ne: null } },
{ user_id: 1 },
).lean();
const sqUserIds = formResponses.map((r) => r.user_id.toString());

if (sqUserIds.length === 0) {
return res.status(200).json({ message: 'No SQ responses found', team1: [], team2: [] });
}

// Step 2: Get all userIds already on a dev team
const teams = await Team.find({}, { 'members.userId': 1 }).lean();
const promotedUserIds = new Set(
teams.flatMap((t) => t.members.map((m) => m.userId.toString())),
);

// Step 3: Filter SQ users who are NOT yet promoted
const pendingUserIds = sqUserIds.filter((id) => !promotedUserIds.has(id));

if (pendingUserIds.length === 0) {
return res.status(200).json({ message: 'All SQ users are promoted', team1: [], team2: [] });
}

// Step 4: Look up userProfile for pending users
const profiles = await UserProfile.find(
{ _id: { $in: pendingUserIds } },
{ firstName: 1, lastName: 1, weeklycommittedHours: 1 },
).lean();

// Step 5: Split by lastName first char A-M / N-Z
const team1Reviewers = [];
const team2Reviewers = [];

profiles.forEach((profile) => {
const firstChar = (profile.lastName || '').charAt(0).toUpperCase();
const reviewerName = `${profile.firstName} ${profile.lastName}`.trim();
const prsNeeded = getPrsNeeded(profile.weeklycommittedHours || 10);

if (firstChar >= 'A' && firstChar <= 'M') {
team1Reviewers.push({ name: reviewerName, prsNeeded });
} else if (firstChar >= 'N' && firstChar <= 'Z') {
team2Reviewers.push({ name: reviewerName, prsNeeded });
}
// names starting with non-alpha chars are ignored — handle manually
});

// Step 6: Upsert prGradingConfigs for Team 1 and Team 2
await PRGradingConfig.findOneAndUpdate(
{ teamName: 'Team 1' },
{
$set: {
reviewerNames: team1Reviewers.map((r) => r.name),
reviewerCount: team1Reviewers.length,
},
},
{ upsert: true, new: true },
);

await PRGradingConfig.findOneAndUpdate(
{ teamName: 'Team 2' },
{
$set: {
reviewerNames: team2Reviewers.map((r) => r.name),
reviewerCount: team2Reviewers.length,
},
},
{ upsert: true, new: true },
);

return res.status(200).json({
message: 'Reviewer sync complete',
team1: team1Reviewers,
team2: team2Reviewers,
});
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error syncing reviewers:', err);
return res.status(500).json({ error: 'Failed to sync reviewers', details: err.message });
}
};
const getAllConfigs = async (req, res) => {
try {
const configs = await PRGradingConfig.find().sort({ createdAt: -1 });
Expand Down Expand Up @@ -57,7 +153,7 @@ const prGradingConfigController = function (PRGradingConfig) {
}
};

return { getAllConfigs, createConfig, deleteConfig };
return { getAllConfigs, createConfig, deleteConfig, syncReviewers };
};

module.exports = prGradingConfigController;
Loading