DEV Community

Cover image for #HackAVP AVPRO : l'espace recruteur qui remplace la boîte mail par un swipe (et explique chaque score)
Luca BEROULE
Luca BEROULE

Posted on

#HackAVP AVPRO : l'espace recruteur qui remplace la boîte mail par un swipe (et explique chaque score)

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

Le constat : boîte mail, pièces jointes, tableau Excel

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

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

La chaîne des 7 modules du #HackAVP

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

L'écran Swipe : la carte du candidat et le détail du score

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

La barre d'envoi différé : « envoi dans 28 s »

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

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

AVPRO sur ordinateur et sur 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)

L'architecture du front

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

Les routes utilisées, écran par écran

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

Le CV au format JSON Resume

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 }]);
};
Enter fullscreen mode Exit fullscreen mode

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 } : {},
},
Enter fullscreen mode Exit fullscreen mode

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 part sans identité vers CV-Matching

  • 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,
  },
};
Enter fullscreen mode Exit fullscreen mode
  • 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)