diff --git a/backend/src/application/services/positionService.ts b/backend/src/application/services/positionService.ts index 1332491..35794cb 100644 --- a/backend/src/application/services/positionService.ts +++ b/backend/src/application/services/positionService.ts @@ -23,7 +23,9 @@ export const getCandidatesByPositionService = async (positionId: number) => { return applications.map(app => ({ fullName: `${app.candidate.firstName} ${app.candidate.lastName}`, currentInterviewStep: app.interviewStep.name, - averageScore: calculateAverageScore(app.interviews) + averageScore: calculateAverageScore(app.interviews), + candidateId: app.candidateId, + applicationId: app.id })); } catch (error) { console.error('Error retrieving candidates by position:', error); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 75428c1..bf8dd83 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,9 +15,11 @@ "@types/node": "^16.18.97", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "axios": "^1.7.2", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", "react": "^18.3.1", + "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.2", "react-bootstrap-icons": "^1.11.4", "react-datepicker": "^6.9.0", @@ -26,6 +28,9 @@ "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/react-beautiful-dnd": "^13.1.8" } }, "node_modules/@adobe/css-tools": { @@ -3741,26 +3746,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@testing-library/dom": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", - "integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -4020,6 +4005,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4154,6 +4148,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.8", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz", + "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", @@ -4163,6 +4166,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -5238,6 +5252,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -6347,6 +6384,14 @@ "postcss": "^8.4" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -9275,6 +9320,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -11736,6 +11794,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -14101,6 +14164,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -14176,6 +14244,11 @@ "performance-now": "^2.1.0" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14265,6 +14338,24 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-bootstrap": { "version": "2.10.2", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.2.tgz", @@ -14474,6 +14565,30 @@ "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" } }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14673,6 +14788,14 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -16535,6 +16658,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16991,6 +17119,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3856c00..76afc7f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,11 @@ "@types/node": "^16.18.97", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "axios": "^1.7.2", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", "react": "^18.3.1", + "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.2", "react-bootstrap-icons": "^1.11.4", "react-datepicker": "^6.9.0", @@ -45,5 +47,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@types/react-beautiful-dnd": "^13.1.8" } } diff --git a/frontend/src/App.css b/frontend/src/App.css index 74b5e05..f34f305 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -32,7 +32,64 @@ from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } + +.back { + border: none; + background: none; + cursor: pointer; + margin-right: 10px; + color: #a5a4a4; + font-size: 24px; +} +.kanban-board { + display: flex; + overflow-x: auto; + padding: 20px; + gap: 20px; +} + +.kanban-column { + flex: 0 0 auto; + width: 320px; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.kanban-title { + text-align: center; + padding: 10px 0; + font-size: 20px; + font-weight: bold; +} + +.kanban-card { + border-radius: 6px; + margin-bottom: 10px; + padding: 0 15px 5px 15px; + cursor: grab; +} + +.kanban-card-title { + font-size: 16px; + font-weight: bold; +} + +.kanban-card:last-child { + margin-bottom: 0; +} + +@media (max-width: 768px) { + .kanban-board { + flex-direction: column; + } + + .kanban-column { + flex: 1 0 auto; + margin-bottom: 20px; + } +} \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 3f27809..93d91fb 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -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 PositionProcessPage from './components/PositionProcessPage'; // Asegúrate de importar el nuevo componente const App = () => { return ( } /> - } /> {/* Agrega esta línea */} + } /> } /> + } /> {/* Nueva ruta para la interfaz kanban */} ); diff --git a/frontend/src/components/KanbanBoard.tsx b/frontend/src/components/KanbanBoard.tsx new file mode 100644 index 0000000..a3aaf86 --- /dev/null +++ b/frontend/src/components/KanbanBoard.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; +import KanbanColumn from './KanbanColumn'; +import { InterviewStep, Candidate } from '../interface/types'; +import { updateCandidate } from '../services/candidateService'; +import '../App.css'; + +interface KanbanBoardProps { + interviewStep: InterviewStep[]; + candidates: Candidate[]; + setCandidates: (candidates: Candidate[]) => void; +} + +const KanbanBoard: React.FC = ({ interviewStep, candidates, setCandidates }) => { + const onDragEnd = async (result: any) => { + const { source, destination } = result; + + if (!destination || (source.droppableId === destination.droppableId && source.index === destination.index)) { + return; // No hacer nada si no hay destino o la tarjeta se movió al mismo lugar + } + + // Encuentra el candidato movido usando el draggableId, que debería ser el candidateId + const movedCandidate = candidates.find(c => c.candidateId === Number(result.draggableId)); + if (!movedCandidate) { + console.error('Candidato no encontrado'); + return; + } + + const newCandidates = Array.from(candidates); + const sourceIndex = candidates.indexOf(movedCandidate); + newCandidates.splice(sourceIndex, 1); + newCandidates.splice(destination.index, 0, movedCandidate); + + const destinationStep = interviewStep.find(step => step.name === destination.droppableId); + if (destinationStep) { + try { + await updateCandidate(movedCandidate.candidateId, destinationStep.id, movedCandidate.applicationId); + movedCandidate.currentInterviewStep = destinationStep.name; + setCandidates(newCandidates); + } catch (error) { + console.error('Error updating candidate:', error); + setCandidates(candidates); // Revertir al estado original si la actualización falla + } + } + }; + + return ( + +
+ {interviewStep.map((flow) => ( + + {(provided) => ( +
+ c.currentInterviewStep === flow.name)} /> + {provided.placeholder} +
+ )} +
+ ))} +
+
+ ); +}; + +export default KanbanBoard; diff --git a/frontend/src/components/KanbanCard.tsx b/frontend/src/components/KanbanCard.tsx new file mode 100644 index 0000000..fdc3203 --- /dev/null +++ b/frontend/src/components/KanbanCard.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Draggable, DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; +import { Candidate } from '../interface/types'; +import { Card, Col } from 'react-bootstrap'; +import '../App.css'; + +interface KanbanCardProps { + candidate: Candidate; + index: number; +} + +const KanbanCard: React.FC = ({ candidate, index }) => { + return ( +
+ + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + + + {candidate.fullName} + + Puntuación media: {candidate.averageScore}
+ {/* ID: {candidate.candidateId}
*/} +
+
+
+ )} +
+
+ ); +}; + +export default KanbanCard; diff --git a/frontend/src/components/KanbanColumn.tsx b/frontend/src/components/KanbanColumn.tsx new file mode 100644 index 0000000..da9c6f0 --- /dev/null +++ b/frontend/src/components/KanbanColumn.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Card, Col } from 'react-bootstrap'; +import { Draggable, DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; +import { Candidate } from '../interface/types'; +import '../App.css'; +import KanbanCard from './KanbanCard'; + +interface KanbanColumnProps { + title: string; + candidates: Candidate[]; +} + +const KanbanColumn: React.FC = ({ title, candidates }) => { + return ( +
+

{title}

+ {candidates.map((candidate, index) => ( + + ))} +
+ ); +}; + +export default KanbanColumn; + diff --git a/frontend/src/components/PositionProcessPage.tsx b/frontend/src/components/PositionProcessPage.tsx new file mode 100644 index 0000000..166d6a0 --- /dev/null +++ b/frontend/src/components/PositionProcessPage.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react'; +import KanbanBoard from './KanbanBoard'; +import { InterviewStep, Candidate, IInterviewFlow } from '../interface/types'; +import { fetchInterviewFlow, fetchCandidates } from '../services/positionService'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Container } from 'react-bootstrap'; + +interface PositionProcessPageProps { + // No más props de match necesarios aquí +} + +const PositionProcessPage: React.FC = () => { + const [interviewStep, setInterviewStep] = useState([]); + const [interviewFlow, setInterviewFlow] = useState(); + const [candidates, setCandidates] = useState([]); + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + useEffect(() => { + if (id) { + const loadData = async () => { + const interviewSteps = await fetchInterviewFlow(id); + const candidatesData = await fetchCandidates(id); + setInterviewStep(interviewSteps.interviewFlow.interviewFlow.interviewSteps); + setInterviewFlow(interviewSteps.interviewFlow); + setCandidates(candidatesData); + console.log(interviewSteps, candidatesData); + }; + + loadData(); + } + }, [id]); + + const handleBack = () => { + navigate('/positions'); + }; + + return ( + +
+ +

{interviewFlow?.positionName}

+
+
+ +
+ ); +}; + +export default PositionProcessPage; diff --git a/frontend/src/components/Positions.tsx b/frontend/src/components/Positions.tsx index 822dccd..b4641e6 100644 --- a/frontend/src/components/Positions.tsx +++ b/frontend/src/components/Positions.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { Card, Container, Row, Col, Form, Button } from 'react-bootstrap'; +import { useNavigate } from 'react-router-dom'; // Change this line type Position = { + id: number; title: string; manager: string; deadline: string; @@ -9,12 +11,23 @@ type Position = { }; 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 Full-Stack Engineer', manager: 'John Doe', deadline: '2024-12-31', status: 'Abierto' }, + { id: 2, title: 'Data Scientist', 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(); // Change this line + + const handleViewProcess = (id: number) => { + // Aquí podrías tener una lógica para obtener el ID basado en el título, por ahora asumimos que el título es único + const position = mockPositions.find(p => p.id === id); + if (position) { + // Supongamos que el título puede ser usado como ID para simplificar + navigate(`/positions/${encodeURIComponent(id)}/process`); // Change this line + } + }; + return (

Posiciones

@@ -57,7 +70,7 @@ const Positions: React.FC = () => { {position.status}
- +
diff --git a/frontend/src/interface/types.ts b/frontend/src/interface/types.ts new file mode 100644 index 0000000..3e241ea --- /dev/null +++ b/frontend/src/interface/types.ts @@ -0,0 +1,26 @@ +export interface Candidate { + fullName: string; + currentInterviewStep: string; + averageScore: number; + candidateId: number; + applicationId: number; +} + +export interface IInterviewFlow { + positionName: string; + interviewFlow: InterviewFlowInterviewFlow; +} + +export interface InterviewFlowInterviewFlow { + id: number; + description: string; + interviewSteps: InterviewStep[]; +} + +export interface InterviewStep { + id: number; + interviewFlowId: number; + interviewTypeId: number; + name: string; + orderIndex: number; +} diff --git a/frontend/src/services/candidateService.js b/frontend/src/services/candidateService.js index dcaed98..f95c267 100644 --- a/frontend/src/services/candidateService.js +++ b/frontend/src/services/candidateService.js @@ -23,4 +23,16 @@ export const sendCandidateData = async (candidateData) => { } catch (error) { throw new Error('Error al enviar datos del candidato:', error.response.data); } -}; \ No newline at end of file +}; + +export const updateCandidate = async (candidateId, currentInterviewStep, applicationId) => { + try { + const response = await axios.put(`http://localhost:3010/candidates/${candidateId}`, { + currentInterviewStep, + applicationId + }); + return response.data; + } catch (error) { + throw new Error('Error al actualizar datos del candidato:', error.response.data); + } +}; diff --git a/frontend/src/services/positionService.ts b/frontend/src/services/positionService.ts new file mode 100644 index 0000000..04bbb1f --- /dev/null +++ b/frontend/src/services/positionService.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +export const fetchInterviewFlow = async (positionId: string) => { + try { + const response = await axios.get(`http://localhost:3010/position/${positionId}/interviewFlow`); + return response.data; + } catch (error:any) { + throw new Error('Error al obtener el flujo de entrevistas:', error.response.data); + } +}; + +export const fetchCandidates = async (positionId: string) => { + try { + const response = await axios.get(`http://localhost:3010/position/${positionId}/candidates`); + return response.data; + } catch (error:any) { + throw new Error('Error al obtener candidatos:', error.response.data); + } +}; diff --git a/prompts/prompts-GDBM.md b/prompts/prompts-GDBM.md new file mode 100644 index 0000000..bafbd1e --- /dev/null +++ b/prompts/prompts-GDBM.md @@ -0,0 +1,140 @@ +## IDE: Cursor + +## Prompts + +1. Quiero que actues como un desarrollador frontend senior. Estamos trabajando en el sistema ATS llamado LTI, en este momento ya se tiene el backend desarrollado @backend y una parte del frontend @frontend. Quiero que crees una interfaz visual `position` la cual aparece cuando se oprime el boton "Ver proceso" en el componente `Positions.tsx`. + En esta interfaz se podra visualizar y gestionar los diferentes candidatos de una posición específica. Quiero que me preguntes que dudas tienes antes de darte las especificaciones y los requisitos para esta interfaz. No escribas codigo aun. + +2. Te voy a resolver las dudas punto por punto para no tener problemas de contexto. No escribas codigo aun. + 1. Especificaciones de la Interfaz: La interfaz debe ser de tipo kanban, mostrando los candidatos como tarjetas en diferentes columnas que representan las fases del proceso de contratación, y pudiendo actualizar la fase en la que se encuentra un candidato solo arrastrando su tarjeta a la nueva columna. Se deben cumplir los siguientes requisitos: + - Se debe mostrar el título de la posición en la parte superior, para dar contexto + - Añadir una flecha a la izquierda del título que permita volver al listado de posiciones + - Deben mostrarse tantas columnas como fases haya en el proceso + - La tarjeta de cada candidato/a debe situarse en la fase correspondiente, y debe mostrar su nombre completo y su puntuación media + - Si es posible, debe mostrarse adecuadamente en móvil (las fases en vertical ocupando todo el ancho) + +3. Listo ahora vamos con el punto 2, no escribas codigo aun. Datos de Candidatos: Contamos con los siguientes endpints: + - `GET /position/:id/interviewFlow`: Este endpoint devuelve información sobre el proceso de contratación para una determinada posición. Output: + ``` + { + "positionName": "Senior backend engineer", + "interviewFlow": { + + "id": 1, + "description": "Standard development interview process", + "interviewSteps": [ + { + "id": 1, + "interviewFlowId": 1, + "interviewTypeId": 1, + "name": "Initial Screening", + "orderIndex": 1 + }, + { + "id": 2, + "interviewFlowId": 1, + "interviewTypeId": 2, + "name": "Technical Interview", + "orderIndex": 2 + }, + { + "id": 3, + "interviewFlowId": 1, + "interviewTypeId": 3, + "name": "Manager Interview", + "orderIndex": 2 + } + ] + } + } + ``` + - `GET /position/:id/candidates`: Este endpoint devuelve todos los candidatos en proceso para una determinada posición, es decir, todas las aplicaciones para un determinado positionID. Output: + ``` + [ + { + "fullName": "John Doe", + "currentInterviewStep": "Technical Interview", + "averageScore": 5 + }, + { + "fullName": "Jane Smith", + "currentInterviewStep": "Technical Interview", + "averageScore": 4 + }, + { + "fullName": "Carlos García", + "currentInterviewStep": "Initial Screening", + "averageScore": 0 + } + ] + ``` + - `PUT /candidate/:id`: Este endpoint actualiza la etapa del candidato movido. Permite modificar la fase actual del proceso de entrevista en la que se encuentra un candidato específico, a través del parámetro "new_interview_step" y proporionando el interview_step_id correspondiente a la columna en la cual se encuentra ahora el candidato. Output: + ``` + { + "message": "Candidate stage updated successfully", + "data": { + "id": 1, + "positionId": 1, + "candidateId": 1, + "applicationDate": "2024-06-04T13:34:58.304Z", + "currentInterviewStep": 3, + "notes": null, + "interviews": [] + } + } + ``` + +4. Listo ahora vamos con el punto 3, no escribas codigo aun. Navegación y Estado: Al hacer clic en "Ver proceso", la interfaz debe mostrarse en una nueva página. +5. Ten en cuenta la siguiente imagen para crear el diseño de la interfaz: + ![image](https://media1-production-mightynetworks.imgix.net/asset/34e1b22b-b8b4-455a-b4cb-c0ddece51640/disen_o_ejemplo_kanban.png?ixlib=rails-4.2.0&fm=jpg&q=75&auto=format) + No escribas codigo aun. Dime si tienes alguna duda adicional o dame los pasos para continuar. + +6. Listo, empecemos con el paso 1. Utiliza las tecnologias y herramientas que tenga ya el proyecto @frontend + **NOTA**: Se siguen iterando los pasos sugeridos hasta que se crean todos los componentes y recursos necesarios. +7. Necesito que ajustes lo siguiente: La lógica o la comunicación con el backend no está en la carpeta @services. Por favor, ajusta las llamadas a los endpoints para que queden igual que en @candidateService.js. +8. Ahora necesito que modifiques el componente @Positions.tsx para que se conecte con la interfaz del Kanban +9. La respuesta correcta del endpoint `GET /position/:id/interviewFlow` es + ``` + { + "interviewFlow": { + "positionName": "Senior Full-Stack Engineer", + "interviewFlow": { + "id": 1, + "description": "Standard development interview process", + "interviewSteps": [ + { + "id": 1, + "interviewFlowId": 1, + "interviewTypeId": 1, + "name": "Initial Screening", + "orderIndex": 1 + }, + { + "id": 2, + "interviewFlowId": 1, + "interviewTypeId": 2, + "name": "Technical Interview", + "orderIndex": 2 + }, + { + "id": 3, + "interviewFlowId": 1, + "interviewTypeId": 3, + "name": "Manager Interview", + "orderIndex": 2 + } + ] + } + } + } + ``` + Corrige los componentes de ser necesario. + +10. Vamos bien. Ahora necesito que apliques estilo a la interfaz para que se vea como en la imagen. Guíate por los estilos de los componentes ya creados en @components y utiliza la librería react-bootstrap. Ten en cuenta lo siguiente: + - La interfaz debe ser responsiva y adaptarse a diferentes dispositivos. + - Debe permitir arrastrar las tarjetas entre las diferentes columnas. + +11. Pasar la logica del `PUT candidates/:id` a @candidateService.js. + +***NOTA***: Surgió un error al mover las tarjetas entre las diferentes columnas: actualizaba al candidato incorrecto o lo colocaba en la columna equivocada. Pasé un tiempo considerable intentando encontrar una solución funcional, pero siempre obtenía respuestas similares que no resolvían el problema. Finalmente, logré solucionarlo, aunque tuve que hacer algunas correcciones manualmente. +