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

"Add Kanban board for managing positions and improve backend functionality" #5

Open
wants to merge 2 commits 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
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"build": "tsc",
"test": "jest",
"prisma:init": "npx prisma init",
"prisma:seed": "ts-node --transpile-only prisma/seed.ts",
"prisma:reset": "npx prisma migrate reset",
"prisma:generate": "npx prisma generate",
"start:prod": "npm run build && npm start"
},
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,55 @@
transform: rotate(360deg);
}
}

.kanban-board {
display: flex;
overflow-x: auto;
padding: 20px;
gap: 20px;
}

.kanban-column {
flex: 0 0 300px;
background-color: #f4f4f4;
border-radius: 8px;
padding: 10px;
height: 90vh;
overflow-y: auto;
}

.kanban-column:hover {
background-color: #e2e2e2;
}

.candidate-card {
background-color: white;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}

.rating-circle {
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-block;
color: white;
line-height: 30px;
text-align: center;
font-weight: bold;
margin: 5px;
}

[data-value='0'] { background-color: #ccc; }
[data-value='1'] { background-color: #f44336; }
[data-value='2'] { background-color: #e91e63; }
[data-value='3'] { background-color: #9c27b0; }
[data-value='4'] { background-color: #673ab7; }
[data-value='5'] { background-color: #3f51b5; }
[data-value='6'] { background-color: #2196f3; }
[data-value='7'] { background-color: #03a9f4; }
[data-value='8'] { background-color: #00bcd4; }
[data-value='9'] { background-color: #009688; }
[data-value='10'] { background-color: #4caf50; }
4 changes: 3 additions & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
import RecruiterDashboard from './components/RecruiterDashboard';
import AddCandidate from './components/AddCandidateForm';
import Positions from './components/Positions';
import Position from './components/Position';

const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<RecruiterDashboard />} />
<Route path="/add-candidate" element={<AddCandidate />} /> {/* Agrega esta línea */}
<Route path="/add-candidate" element={<AddCandidate />} />
<Route path="/positions" element={<Positions />} />
<Route path="/position" element={<Position />} />
</Routes>
</BrowserRouter>
);
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/CandidateCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { Candidate } from '../types/index';
import RatingCircle from './RatingCircle';
import '../App.css';

const CandidateCard: React.FC<{ candidate: Candidate }> = ({ candidate }) => {
return (
<div className="candidate-card">
<p>{candidate.firstName} {candidate.lastName}</p>
<p>Score: {candidate.averageScore}</p>
<RatingCircle score={candidate.averageScore} />
</div>
);
};

export default CandidateCard;
17 changes: 17 additions & 0 deletions frontend/src/components/KanbanBoard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import KanbanColumn from './KanbanColumn';
import { Candidate } from '../types/index';
import '../App.css';

const KanbanBoard: React.FC<{ phases: string[]; candidates: Candidate[] }> = ({ phases, candidates }) => {
return (
<div className="kanban-board">
{phases.map(phase => (
<KanbanColumn key={phase} phase={phase} candidates={candidates.filter(c => c.phase === phase)} />
))}
</div>
);
};

export default KanbanBoard;

16 changes: 16 additions & 0 deletions frontend/src/components/KanbanColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// src/components/KanbanColumn.tsx
import React from 'react';
import CandidateCard from './CandidateCard';
import '../App.css';
import { Candidate } from '../types';

const KanbanColumn: React.FC<{ phase: string; candidates: Candidate[] }> = ({ phase, candidates }) => {
return (
<div className="kanban-column">
<h2>{phase}</h2>
{candidates.map(candidate => <CandidateCard key={candidate.id} candidate={candidate} />)}
</div>
);
};

export default KanbanColumn;
28 changes: 28 additions & 0 deletions frontend/src/components/Position.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import PositionHeader from './PositionHeader';
import KanbanBoard from './KanbanBoard';

const Position: React.FC = () => {
const location = useLocation();
const { position } = location.state;

// Mock data for candidates
const candidates = [
{ id: 1, firstName: "Jane", lastName: "Smith", email: "[email protected]", phase: "Technical Interview", averageScore: 4 },
{ id: 2, firstName: "Carlos", lastName: "García", email: "[email protected]", phase: "Initial Screening", averageScore: 0 },
{ id: 3, firstName: "John", lastName: "Doe", email: "[email protected]", phase: "Manager Interview", averageScore: 5 }
];

// Phases extracted from candidates data
const phases = Array.from(new Set(candidates.map(candidate => candidate.phase)));

return (
<div>
<PositionHeader title={position.title} />
<KanbanBoard phases={phases} candidates={candidates} />
</div>
);
};

export default Position;
16 changes: 16 additions & 0 deletions frontend/src/components/PositionHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft } from 'react-bootstrap-icons';

const PositionHeader: React.FC<{ title: string }> = ({ title }) => {
const navigate = useNavigate();

return (
<div>
<ArrowLeft onClick={() => navigate(-1)} />
<h1>{title}</h1>
</div>
);
};

export default PositionHeader;
23 changes: 12 additions & 11 deletions frontend/src/components/Positions.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import React from 'react';
import { Card, Container, Row, Col, Form, Button } from 'react-bootstrap';

type Position = {
title: string;
manager: string;
deadline: string;
status: 'Abierto' | 'Contratado' | 'Cerrado' | 'Borrador';
};
import { useNavigate } from 'react-router-dom';
import { Position } from '../types';

const mockPositions: Position[] = [
{ title: 'Senior Backend Engineer', manager: 'John Doe', deadline: '2024-12-31', status: 'Abierto' },
{ title: 'Junior Android Engineer', manager: 'Jane Smith', deadline: '2024-11-15', status: 'Contratado' },
{ title: 'Product Manager', manager: 'Alex Jones', deadline: '2024-07-31', status: 'Borrador' }
{ id: 1, title: 'Senior Backend Engineer', manager: 'John Doe', deadline: '2024-12-31', status: 'Abierto' },
{ id: 2, title: 'Junior Android Engineer', manager: 'Jane Smith', deadline: '2024-11-15', status: 'Contratado' },
{ id: 3, title: 'Product Manager', manager: 'Alex Jones', deadline: '2024-07-31', status: 'Borrador' }
];

const Positions: React.FC = () => {
const navigate = useNavigate();

const handleViewProcess = (position: Position) => {
navigate('/position', { state: { position } });
};

return (
<Container className="mt-5">
<h2 className="text-center mb-4">Posiciones</h2>
Expand Down Expand Up @@ -57,7 +58,7 @@ const Positions: React.FC = () => {
{position.status}
</span>
<div className="d-flex justify-content-between mt-3">
<Button variant="primary">Ver proceso</Button>
<Button variant="primary" onClick={() => handleViewProcess(position)}>Ver proceso</Button>
<Button variant="secondary">Editar</Button>
</div>
</Card.Body>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/components/RatingCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

const RatingCircle = ({ score }: { score: number }) => {
return (
<div>
{Array.from({ length: score }, (_, index) => (
<div key={index} className="rating-circle" data-value={score}></div>
))}
</div>
);
};

export default RatingCircle;
35 changes: 32 additions & 3 deletions frontend/src/services/candidateService.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import axios from 'axios';

// Set base url to axios
axios.defaults.baseURL = 'http://localhost:3010';

export const uploadCV = async (file) => {
const formData = new FormData();
formData.append('file', file);

try {
const response = await axios.post('http://localhost:3010/upload', formData, {
const response = await axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
Expand All @@ -18,9 +21,35 @@ export const uploadCV = async (file) => {

export const sendCandidateData = async (candidateData) => {
try {
const response = await axios.post('http://localhost:3010/candidates', candidateData);
const response = await axios.post('/candidates', candidateData);
return response.data;
} catch (error) {
throw new Error('Error al enviar datos del candidato:', error.response.data);
}
};
};

export const fetchCandidates = async (positionId) => {
try {
const response = await axios.get(`/position/${positionId}/candidates`);
return response.data;
} catch (error) {
throw new Error('Error al obtener los candidatos:', error.response.data);
}
};

export const updateCandidateStage = async (candidateId, stageData) => {
try {
const response = await axios.patch(`/candidates/${candidateId}/stage`, stageData);
return response.data;
} catch (error) {
throw new Error('Error al actualizar la etapa del candidato:', error.response.data);
}
};

axios.interceptors.response.use(
response => response,
error => {
const message = error.response?.data?.message || 'Error desconocido';
throw new Error(message);
}
);
37 changes: 37 additions & 0 deletions frontend/src/services/positionService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import axios from 'axios';

export const fetchAllPositions = async () => {
try {
const response = await axios.get('/positions');
return response.data;
} catch (error) {
throw new Error('Error fetching positions:', error.response.data);
}
};

export const addPosition = async (positionData) => {
try {
const response = await axios.post('/positions', positionData);
return response.data;
} catch (error) {
throw new Error('Error adding position:', error.response.data);
}
};

export const updatePosition = async (positionId, positionData) => {
try {
const response = await axios.patch(`/positions/${positionId}`, positionData);
return response.data;
} catch (error) {
throw new Error('Error updating position:', error.response.data);
}
};

export const deletePosition = async (positionId) => {
try {
const response = await axios.delete(`/positions/${positionId}`);
return response.data;
} catch (error) {
throw new Error('Error deleting position:', error.response.data);
}
};
16 changes: 16 additions & 0 deletions frontend/src/types/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface Candidate {
id: number;
firstName: string;
lastName: string;
email: string;
phase: string;
averageScore: number;
}

export interface Position {
id: number;
title: string;
manager: string;
deadline: string;
status: 'Abierto' | 'Contratado' | 'Cerrado' | 'Borrador';
}
17 changes: 17 additions & 0 deletions lti.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"folders": [
{
"name": "Frontend",
"path": "./frontend"
},
{
"name": "Backend",
"path": "./backend"
},
{
"name": "ROOT",
"path": "."
}
]
}

Loading