Une candidature reçue n'est pas une candidature traitée.
Côté candidat, Adopte un Job permet de swiper les vrais postes de l'OPT-NC et de comprendre pourquoi ils nous correspondent (l'article de Bastian). Mais un swipe à droite, ça crée une candidature… et quelqu'un, de l'autre côté, doit la lire, la comprendre et y répondre.
C'est ce qu'on a construit : AVPRO, l'espace recruteur d'Adopte un Job.
- 🔗 Code source : github.com/DamienJmt/avpro
- 🎬 Démo vidéo : 3 min 38 (plus bas)
- 👥 Équipe : Damien & Luca, module MS6 – AVPRO-NC du #HackAVP
🎯 Le contexte : recruter, c'est encore souvent laborieux
Aujourd'hui, une candidature à un avis de vacance de poste (AVP), c'est très souvent :
- 📧 un mail, parfois un « RE: RE: Candidature », parfois une relance ;
- 📎 des pièces jointes à ouvrir une par une :
CV_final_v3.pdf,CV(1).docx,scan_cv.jpg… ; - 📊 un tableau Excel pour savoir qui a été vu, qui a été relancé, qui est un doublon ;
- ⏳ et au bout, des candidats qui attendent des semaines sans savoir où ils en sont.
Le recruteur passe son temps à trier au lieu d'évaluer. Et le candidat, lui, n'a aucune visibilité.
💡 La réponse : AVPRO
AVPRO remplace tout ça par un seul outil :
| Avant | Avec AVPRO | |
|---|---|---|
| 📥 Réception | des mails éparpillés | toutes les candidatures au même endroit |
| 🔍 Évaluation | ouvrir chaque CV, comparer à la fiche de poste | un score de compatibilité expliqué, compétence par compétence |
| ✋ Décision | un mail à rédiger | un geste : à droite on présélectionne, à gauche on écarte |
| 🔔 Réponse | des semaines d'attente | le candidat voit la décision aussitôt dans son app |
| 📅 Entretiens | un fil de mails | suivis dans l'outil, sans un seul mail |
L'idée est volontairement symétrique à Adopte un Job : le candidat swipe des postes, le recruteur swipe des candidatures.
🎬 La démo
Les données de la démo (candidats, AVP, scores) sont fictives.
🧩 Notre place dans le projet commun du Master MIAGE
Le #HackAVP réunit sept groupes, chacun sur un maillon de la chaîne :
| Module | Projet | Équipe |
|---|---|---|
| MS1 | CV Forge | Florian |
| MS2 | HACKAVPI | Dan & Thimotée |
| MS3 | Adopte un Job | Bastian |
| MS4 | CV Generator | Anthony & Kim |
| MS5 | CV-Matching | Jo-Michel |
| MS6 | AVPRO-NC | Damien & Luca |
| MS7 | Insight Squad | Élodie & Matthieu |
Nous, c'est MS6, le côté recruteur. AVPRO n'a ni base de données ni serveur à lui : il s'appuie sur trois autres modules.
- MS2 – HACKAVPI : l'API commune (comptes, AVP, candidatures, profils, décisions, entretiens).
- MS3 – Adopte un Job : les candidats, leur CV au format JSON Resume et les critères qu'ils ont saisis (métier visé, contrat, zone, disponibilité).
- MS5 – CV-Matching : le score de compatibilité entre un CV et un AVP.
Pour tout savoir sur l'API commune, l'article de l'équipe HACKAVPI :
Concrètement : un swipe à droite dans Adopte un Job crée une candidature dans HACKAVPI, AVPRO la récupère, demande son score à CV-Matching, et la décision du recruteur repart dans HACKAVPI, où le candidat la voit.
🧭 Le parcours recruteur, étape par étape
1️⃣ Je me connecte
Un écran simple : adresse et mot de passe. Pas d'inscription libre, un compte recruteur ne peut être créé que par un recruteur ou un admin.
À la connexion, l'API renvoie un jeton JWT valable 1 h. AVPRO lit le rôle dans le jeton : si c'est un compte candidat, il est poliment renvoyé vers Adopte un Job. Quand le jeton expire, on revient à la connexion, sans écran cassé.
2️⃣ Je swipe les candidatures
Une carte = une candidature à traiter. On y voit d'un coup d'œil :
- le score de compatibilité (« 88 % · très bonne adéquation ») et le nombre de compétences du poste couvertes ;
- l'expérience, le niveau de formation, la disponibilité ;
- des repères verts quand le candidat vise ce métier ou cherche dans cette zone ;
- ses compétences, avec en vert celles que le matching a reconnues dans le poste.
À droite pour présélectionner, à gauche pour écarter : à la souris, au doigt, ou avec les boutons sous la carte.
3️⃣ Je comprends le score
À côté de la carte (ou dans une feuille sur téléphone), le panneau de détail répond à la vraie question : pourquoi ce score ?
- Points forts : chaque compétence attendue par l'AVP, avec la phrase du CV qui la prouve (« Développement web React / TypeScript — “Développeuse React et Node depuis 4 ans” »).
- À vérifier : ce qui est seulement approchant (« Bases de données SQL — au plus proche : “PostgreSQL” »).
- Savoir-être, à vérifier en entretien : ce qu'un CV ne peut pas prouver.
- Prérequis : permis, diplôme… marqués rempli, non rempli ou à vérifier.
Et à part, ce que le candidat recherche : métier, contrat, lieu, disponibilité, affichés en oui / non / inconnu.
💡 Choix assumé : les critères du candidat ne font pas de score. S'il ne vise pas ce contrat ou cette zone, la carte l'indique (« hors de ses critères ») mais le recruteur peut le retenir quand même. L'outil éclaire, le recruteur décide.
4️⃣ Je filtre
Une recherche (nom, métier, intitulé de poste) et des filtres Poste · Direction · Lieu du poste · Options (profil renseigné, CV déposé).
Plusieurs choix dans une même catégorie, c'est l'un ou l'autre ; d'une catégorie à l'autre, c'est les deux. Et chaque case annonce combien de candidats elle garderait avant même qu'on la coche : plus de filtre qui vide l'écran par surprise.
5️⃣ Je décide… et j'ai 30 secondes pour changer d'avis
Dans l'API, une décision est définitive et le candidat la voit immédiatement. Un swipe trop rapide ne doit pas devenir un refus envoyé à quelqu'un.
Alors une décision ne part qu'après 30 secondes. Pendant ce délai, une barre en bas de l'écran affiche « envoi dans 28 s » avec Annuler et Envoyer maintenant ; le bouton Retour du swipe et l'onglet Candidats permettent aussi d'annuler. Et si on ferme la page, les décisions en attente partent quand même.
6️⃣ Je suis mes candidatures
L'onglet Candidats reprend tout, par statut : À traiter, Présélectionné, Toutes. Chaque ligne garde sa case de compatibilité, et une candidature retirée par le candidat apparaît grisée.
| Statut dans l'API | Dans AVPRO |
|---|---|
EN_ATTENTE |
À traiter |
VALIDEE |
Présélectionné |
REJETEE |
Rejetée |
ANNULEE |
Retirée par le candidat |
VALIDEE + entretienAProg
|
Entretien |
7️⃣ Je valide un entretien
Un profil présélectionné peut passer en entretien, après une confirmation. Il rejoint l'onglet Entretiens, avec un récapitulatif du poste (intitulé, référence, direction, lieu, contrat, début, date limite) et de la candidature (profil, contact, expérience, formation, disponibilité, compatibilité).
Choix de périmètre : AVPRO marque l'entretien à programmer, il ne gère pas l'agenda. La date et le lieu restent l'affaire du recruteur.
8️⃣ Et aussi
- AVP : les avis de vacance de poste, les ouverts d'abord, triés par date limite, avec le nombre de candidatures à traiter.
- Profil : l'adresse du compte et le changement de mot de passe par un code reçu par mail.
📱 Responsive : ordinateur et téléphone
Un recruteur ne trie pas toujours à son bureau. Sur grand écran, AVPRO affiche un menu latéral et le détail du score en panneau à côté de la carte. Sur téléphone, les onglets passent en bas de l'écran, et le détail s'ouvre d'un geste, en feuille, depuis le score de la carte.
🛠️ Sous le capot (pour les devs)
La stack
- React 19 + TypeScript strict + Vite 7, sortie 100 % statique.
-
Même socle et même design que l'app candidat : les CSS d'Adopte un Job sont repris tels quels, pour une fusion simple dans
beta/. - Pas de base, pas de serveur à nous : tout passe par l'API commune (MS2) et par CV-Matching (MS5).
Un seul fichier parle à l'API
src/api.ts est le seul fichier qui appelle HACKAVPI. Il traduit leurs réponses dans les formes de src/types.ts (Avp, Candidat, Candidature) : les écrans ne connaissent jamais le format de l'API. Quand l'API change, un seul fichier s'adapte.
| Écran | Route |
|---|---|
| Connexion | POST /auth/login |
| AVP | GET /avp |
| Swipe, Candidats |
GET /candidatures · GET /profils · GET /profils/{id}/cv
|
| Décision |
POST /swipes/recruteurs/candidature/{id} { estLike }
|
| Entretien |
PATCH /candidatures/{id}/entretien-a-prog { entretienAProg }
|
| Compatibilité |
POST /v1/score { reference, cv } (CV-Matching) |
| Profil |
POST /auth/forgot-password · POST /auth/reset-password
|
Les trois listes (AVP, candidatures, profils) sont chargées en parallèle, toutes pages lues (100 par page), au démarrage et à chaque « Actualiser ».
Les données : plusieurs formats de CV, une seule forme
Dans l'API, plusieurs formats de CV circulent :
- le JSON Resume écrit par l'app candidat, avec les critères dans
meta.criteres; - le JSON Resume produit par la lecture du CV déposé (
cvJsonGenere) ; - les formats courts des comptes de démonstration (
titre,competences: string[],experiences: {poste, annees}[]…).
api.ts les ramène tous à une seule forme Candidat : années d'expérience cumulées, niveau de formation (Bac → Bac+5), compétences dédoublonnées, critères. Les zones sont recalculées comme dans l'app candidat (Grand Nouméa, Sud, Nord, Îles) pour comparer un AVP aux zones visées.
La décision différée
Le cœur du « droit à l'erreur » tient en quelques lignes dans App.tsx :
/* Une décision du recruteur est définitive dans l'API et le candidat la voit
aussitôt : elle n'est envoyée qu'après ce délai, annulable jusque-là. */
const DELAI = 30_000;
const decider = (id: number, ok: boolean) => {
const avant = minuteurs.current.get(id);
if (avant) window.clearTimeout(avant.t);
maj(id, ok ? "preselectionne" : "rejete"); // l'écran change tout de suite
const t = window.setTimeout(() => void envoie(id, ok), DELAI);
minuteurs.current.set(id, { t, ok });
setAttente((l) => [...l.filter((x) => x.id !== id), { id, ok, echeance: Date.now() + DELAI }]);
};
Et pour ne rien perdre à la fermeture de l'onglet, les décisions en attente partent sur pagehide avec un fetch(..., { keepalive: true }), que le navigateur termine seul.
Le matching, à la demande
src/matching.ts appelle CV-Matching uniquement quand un écran affiche un score, trois appels à la fois au plus, et garde les résultats pour la session (un candidat ne change pas de CV pendant qu'on le regarde). Les composants s'abonnent au cache avec useSyncExternalStore ; « Actualiser » le vide.
CORS et clé d'API
Le serveur commun n'envoie pas d'en-têtes CORS, et CV-Matching exige une clé X-API-Key. En développement, le proxy Vite règle les deux :
// vite.config.ts
"/api-matching": {
target: env.MATCHING_URL || "https://myz0-cv-matching.hf.space",
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api-matching/, ""),
// MATCHING_API_KEY vient de .env.local, sans préfixe VITE_ :
// elle reste côté serveur et n'est jamais dans le code envoyé au navigateur.
headers: env.MATCHING_API_KEY ? { "X-API-Key": env.MATCHING_API_KEY } : {},
},
En production, il faudra un relais du même genre (comme le relais.php d'Adopte un Job), déclaré dans VITE_API_URL.
🔐 Données personnelles
- Le CV envoyé à CV-Matching part sans identité : ni nom, ni adresse, ni téléphone, ni profils en ligne. Le service n'en a pas besoin, elles n'ont pas à quitter l'API commune. On retire même le nom quand il apparaît dans le résumé (« Léa Wamytan, 3 ans d'expérience… »).
// api.ts — extrait de cvPourMatching()
const { meta: _meta, $schema: _schema, references: _refs, ...reste } = doc;
return {
...reste,
basics: {
label: b.label,
summary: sansNom(b.summary), // le nom du candidat est retiré du résumé
location: objet(b.location).city ? { city: objet(b.location).city } : undefined,
},
};
- La clé du matching n'est jamais dans le navigateur (voir le proxy plus haut).
-
Seule la session vit dans le navigateur : le jeton, en
sessionStorage, effacé à la fermeture de l'onglet. - Les données de la démo sont fictives.
⚖️ Nos arbitrages
Quelques décisions qu'on a dû trancher, et pourquoi :
- Un score qui s'explique, plutôt qu'un classement opaque. Un pourcentage seul ne dit rien ; les preuves tirées du CV, si.
- Les critères du candidat informent, ils ne filtrent pas. Exclure automatiquement quelqu'un parce qu'il a coché « CDI » sur un CDD, c'est passer à côté de profils.
- 30 secondes de délai plutôt qu'une confirmation à chaque swipe. Une popup « Êtes-vous sûr ? » casse le rythme du tri ; un délai annulable protège sans gêner.
- Pas d'agenda d'entretien. On marque l'entretien à programmer, on ne réinvente pas le calendrier du recruteur.
- Aucune base à nous. Une seule source de vérité, l'API commune : ce que voit le recruteur est exactement ce que voit le candidat.
🚀 Et ensuite ?
-
Fusionner AVPRO dans l'app Adopte un Job (
beta/) : même socle, mêmes CSS, il reste à importer les styles communs et à ajouter quelques icônes au sprite. - Un relais de production pour le CORS et la clé du matching, comme côté candidat.
- Le renouvellement de session : le jeton dure 1 h sans renouvellement aujourd'hui.
- Le changement d'adresse du compte, que l'API ne permet pas encore.
🏁 Pour finir
Adopte un Job aide le candidat à trouver le poste qui lui correspond. AVPRO aide le recruteur à trouver les candidats qui correspondent à son poste, et à leur répondre vite.
Fini la boîte mail qui déborde : chaque candidature arrive au même endroit, avec un score qui s'explique, une décision en un geste et un candidat qui sait où il en est.
Et vous, côté recruteur ou côté candidat : qu'est-ce qui vous a le plus fait perdre de temps dans un recrutement ? Dites-le-nous en commentaire 👇
Damien & Luca — AVPRO-NC (MS6), #HackAVP, Master MIAGE · IAE Nouvelle-Calédonie × OPT-NC
🔗 Code source · HACKAVPI, l'API commune (MS2) · Adopte un Job, l'app candidat (MS3)











Top comments (0)