Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADFR Entrega Backend #4

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
70 changes: 70 additions & 0 deletions backend/src/application/services/candidateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { validateCandidateData } from '../validator';
import { Education } from '../../domain/models/Education';
import { WorkExperience } from '../../domain/models/WorkExperience';
import { Resume } from '../../domain/models/Resume';
import {
CandidatePositionResponse,
CandidatePositionPaginatedResponse
} from '../../presentation/types/responses/candidateResponses';
import { Application } from '../../domain/models/Application';
import { InterviewStep } from '../../domain/models/InterviewStep';

export const addCandidate = async (candidateData: any) => {
try {
Expand Down Expand Up @@ -63,3 +69,67 @@ export const findCandidateById = async (id: number): Promise<Candidate | null> =
throw new Error('Error al recuperar el candidato');
}
};

export const findCandidatesByPosition = async (
positionId: number,
page: number = 1,
pageSize: number = 10
): Promise<CandidatePositionPaginatedResponse> => {
const skip = (page - 1) * pageSize;

const [candidates, total] = await Candidate.findByPosition(positionId, skip, pageSize);

const formattedCandidates: CandidatePositionResponse[] = candidates.map(app => {
const scores = app.interviews
.map(interview => interview.score)
.filter((score): score is number => score !== null);

const averageScore = scores.length > 0
? scores.reduce((a, b) => a + b, 0) / scores.length
: null;

return {
name: `${app.candidate.firstName} ${app.candidate.lastName}`,
currentInterviewStep: app.currentInterviewStep,
averageScore
};
});

// Ordenar por puntuación media (null al final)
formattedCandidates.sort((a, b) => {
if (a.averageScore === null) return 1;
if (b.averageScore === null) return -1;
return b.averageScore - a.averageScore;
});

return {
data: formattedCandidates,
total,
page,
pageSize
};
};

export const updateCandidateInterviewStage = async (
candidateId: number,
stageId: number
): Promise<Application | null> => {
try {
// Verificar si la etapa existe
const interviewStep = await InterviewStep.findOne(stageId);
if (!interviewStep) {
throw new Error('Interview stage not found');
}

// Obtener el candidato y actualizar su etapa
const candidate = await Candidate.findOne(candidateId);
if (!candidate) {
return null;
}

return await candidate.updateInterviewStage(stageId);
} catch (error) {
console.error('Error in updateCandidateInterviewStage:', error);
throw error;
}
};
95 changes: 92 additions & 3 deletions backend/src/domain/models/Candidate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PrismaClient, Prisma } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import { PrismaClientInitializationError } from '@prisma/client/runtime/library';
import { Education } from './Education';
import { WorkExperience } from './WorkExperience';
import { Resume } from './Resume';
Expand Down Expand Up @@ -98,7 +99,7 @@ export class Candidate {
});
} catch (error: any) {
console.log(error);
if (error instanceof Prisma.PrismaClientInitializationError) {
if (error instanceof PrismaClientInitializationError) {
// Database connection error
throw new Error('No se pudo conectar con la base de datos. Por favor, asegúrese de que el servidor de base de datos esté en ejecución.');
} else if (error.code === 'P2025') {
Expand All @@ -116,7 +117,7 @@ export class Candidate {
});
return result;
} catch (error: any) {
if (error instanceof Prisma.PrismaClientInitializationError) {
if (error instanceof PrismaClientInitializationError) {
// Database connection error
throw new Error('No se pudo conectar con la base de datos. Por favor, asegúrese de que el servidor de base de datos esté en ejecución.');
} else {
Expand All @@ -126,6 +127,48 @@ export class Candidate {
}
}

async updateInterviewStage(stageId: number): Promise<Application | null> {
try {
// Encontrar la aplicación activa del candidato
const application = await prisma.application.findFirst({
where: {
candidateId: this.id,
// Puedes agregar condiciones adicionales aquí si es necesario
// como status !== 'CLOSED'
}
});

if (!application) {
return null;
}

// Actualizar la aplicación con la nueva etapa
const updatedApplication = await prisma.application.update({
where: { id: application.id },
data: {
currentInterviewStep: stageId,
// Puedes agregar campos adicionales aquí
// como lastUpdated: new Date()
},
include: {
interviews: true,
position: true
}
});

// Actualizar la aplicación en la lista de aplicaciones del candidato
const applicationIndex = this.applications.findIndex(app => app.id === application.id);
if (applicationIndex !== -1) {
this.applications[applicationIndex] = new Application(updatedApplication);
}

return new Application(updatedApplication);
} catch (error) {
console.error('Error updating interview stage:', error);
throw error;
}
}

static async findOne(id: number): Promise<Candidate | null> {
const data = await prisma.candidate.findUnique({
where: { id: id },
Expand Down Expand Up @@ -160,4 +203,50 @@ export class Candidate {
if (!data) return null;
return new Candidate(data);
}

static async findByPosition(
positionId: number,
skip: number,
take: number
): Promise<[Array<{
currentInterviewStep: number;
candidate: {
firstName: string;
lastName: string;
};
interviews: Array<{
score: number | null;
}>;
}>, number]> {
const [candidates, total] = await Promise.all([
prisma.application.findMany({
where: {
positionId: positionId
},
select: {
currentInterviewStep: true,
candidate: {
select: {
firstName: true,
lastName: true
}
},
interviews: {
select: {
score: true
}
}
},
skip,
take
}),
prisma.application.count({
where: {
positionId: positionId
}
})
]);

return [candidates, total];
}
}
2 changes: 2 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dotenv from 'dotenv';
import candidateRoutes from './routes/candidateRoutes';
import { uploadFile } from './application/services/fileUploadService';
import cors from 'cors';
import positionRoutes from './routes/positionRoutes';

// Extender la interfaz Request para incluir prisma
declare global {
Expand Down Expand Up @@ -38,6 +39,7 @@ app.use(cors({

// Import and use candidateRoutes
app.use('/candidates', candidateRoutes);
app.use('/positions', positionRoutes);

// Route for file uploads
app.post('/upload', uploadFile);
Expand Down
61 changes: 59 additions & 2 deletions backend/src/presentation/controllers/candidateController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Request, Response } from 'express';
import { addCandidate, findCandidateById } from '../../application/services/candidateService';
import { addCandidate, findCandidateById, updateCandidateInterviewStage } from '../../application/services/candidateService';
import { PrismaClient } from '@prisma/client';
import { findCandidatesByPosition } from '../../application/services/candidateService';

const prisma = new PrismaClient();

export const addCandidateController = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -31,4 +35,57 @@ export const getCandidateById = async (req: Request, res: Response) => {
}
};

export { addCandidate };
export const getCandidatesByPosition = async (req: Request, res: Response) => {
try {
const positionId = parseInt(req.params.positionId);
const page = parseInt(req.query.page as string) || 1;
const pageSize = parseInt(req.query.pageSize as string) || 10;

if (isNaN(positionId)) {
return res.status(400).json({ error: 'Invalid position ID format' });
}

// Verificar si la posición existe
const positionExists = await prisma.position.findUnique({
where: { id: positionId }
});

if (!positionExists) {
return res.status(404).json({ error: 'Position not found' });
}

const candidates = await findCandidatesByPosition(positionId, page, pageSize);
res.json(candidates);
} catch (error) {
console.error('Error fetching candidates:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
};

export const updateCandidateStage = async (req: Request, res: Response) => {
try {
const candidateId = parseInt(req.params.id);
const { stageId } = req.body;

if (isNaN(candidateId)) {
return res.status(400).json({ error: 'Invalid candidate ID format' });
}

if (!stageId || isNaN(stageId)) {
return res.status(400).json({ error: 'Invalid stage ID' });
}

const result = await updateCandidateInterviewStage(candidateId, stageId);

if (!result) {
return res.status(404).json({ error: 'Candidate or application not found' });
}

res.json({ message: 'Interview stage updated successfully', data: result });
} catch (error) {
console.error('Error updating candidate stage:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
};

export { addCandidate };
12 changes: 12 additions & 0 deletions backend/src/presentation/types/responses/candidateResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface CandidatePositionResponse {
name: string;
currentInterviewStep: number;
averageScore: number | null;
}

export interface CandidatePositionPaginatedResponse {
data: CandidatePositionResponse[];
total: number;
page: number;
pageSize: number;
}
26 changes: 15 additions & 11 deletions backend/src/routes/candidateRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { Router } from 'express';
import { addCandidate, getCandidateById } from '../presentation/controllers/candidateController';
import {
addCandidate,
getCandidateById,
updateCandidateStage
} from '../presentation/controllers/candidateController';

const router = Router();

router.post('/', async (req, res) => {
try {
// console.log(req.body); //Just in case you want to inspect the request body
const result = await addCandidate(req.body);
res.status(201).send(result);
} catch (error) {
if (error instanceof Error) {
res.status(400).send({ message: error.message });
} else {
res.status(500).send({ message: "An unexpected error occurred" });
try {
const result = await addCandidate(req.body);
res.status(201).send(result);
} catch (error) {
if (error instanceof Error) {
res.status(400).send({ message: error.message });
} else {
res.status(500).send({ message: "An unexpected error occurred" });
}
}
}
});

router.get('/:id', getCandidateById);
router.put('/:id/stage', updateCandidateStage);

export default router;
8 changes: 8 additions & 0 deletions backend/src/routes/positionRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Router } from 'express';
import { getCandidatesByPosition } from '../presentation/controllers/candidateController';

const router = Router();

router.get('/:positionId/candidates', getCandidatesByPosition);

export default router;
Loading