gestion de téléprospection
109 FICHIERS← secteur 7-Gle tableauCadence — plateau de téléprospection « reprise véhicule »
Application de gestion de téléprospection pour le rachat de voitures à des particuliers. Elle couvre la chaîne complète décrite dans le brief : appeler des prospects vendeurs, qualifier l'intention et le véhicule selon un barème explicite, puis transmettre au client des leads qualifiés, consentants et vérifiables — avec la facturation qui va avec.
Réponse à l'offre codeur.com n° 488950 *(Gestion de téléprospection)*.
Démarrer
bun install bun dev # http://localhost:3000
Aucune variable d'environnement, aucun service externe. Au premier chargement, un jeu de démonstration déterministe est généré dans .data/store.json : 238 leads, ~560 appels, 5 campagnes, 8 agents, 3 lots déjà livrés. Le bouton Régénérer le jeu de démonstration (page *Barème & RGPD*) le remet à zéro.
bun run verify # typegen + tsc --noEmit + eslint + next build bun test # 36 tests unitaires (parseur CSV, téléphones, import, Bloctel) bun tests/e2e-import.ts # parcours d'import piloté au navigateur (serveur dev requis)
tests/e2e-import.tsremet le jeu de démonstration à zéro avant de s'exécuter, pour rester rejouable. Il prend l'URL en argument (bun tests/e2e-import.ts http://localhost:3001).
Ce que fait l'application
| Écran | Rôle |
|---|---|
| Tableau de bord | Production du jour vs objectif, taux de décroché et de qualification sur 30 j, activité 14 jours, entonnoir de conversion, rappels dus, avancement des campagnes. |
Poste d'appel (/dialer) | Le poste de travail du téléprospecteur : file priorisée, fiche prospect, script de campagne avec variables résolues, traitement des objections, formulaire de qualification, chrono d'appel et dispositions. |
| Leads | Recherche et filtres (statut, campagne, agent, source, catégorie), score et blocages sur chaque ligne, export CSV de la sélection courante, fiche détaillée avec historique d'appels. |
| Campagnes | Objectifs, tarif par lead, périmètre, script éditable, entonnoir et issues d'appel par campagne. |
| Équipe | Production individuelle sur la période choisie : appels, contacts établis, taux de décroché, qualifiés, temps en ligne, CA généré. |
| Transmission | Le livrable : composition d'un lot, revérification de l'éligibilité au moment de la livraison, historique des lots, export CSV par lot. |
Import de fichier (/import) | L'entrée de l'entonnoir : dépôt d'un CSV de prospection, correspondance des colonnes détectée puis corrigeable, prévisualisation ligne par ligne avec motif, écriture après confirmation. Suivi de l'échéance Bloctel de chaque fichier. |
| Barème & RGPD | Pondérations, seuil, plafonds véhicule, règles de conformité — avec la distribution des scores et l'effet du seuil en direct. |
La file d'appel est priorisée, pas alphabétique
fileDAppel (lib/queries.ts) classe dans cet ordre :
- rappels dus (les retards passent devant, proportionnellement au retard) ;
- nouveaux leads, par score décroissant ;
- reprises d'injoignables, sous un plafond de 5 tentatives ;
- qualifiés bloqués, pour aller chercher l'information manquante.
Les leads d'une campagne non active n'y entrent jamais.
Le barème de qualification
Le cœur métier. Un lead vaut de l'argent pour le client s'il est *réellement* vendeur — pas s'il a simplement décroché. scorerLead (lib/scoring.ts) est une fonction pure qui rend, pour chaque dossier :
- six critères pondérés, ramenés sur 100, chacun accompagné de ce qui a été
mesuré en clair (« 12 900 € demandés pour 9 850 € de cote (+31 %) ») ;
- une liste de blocages rédhibitoires, indépendants du score : consentement
RGPD absent, inscription sur la liste d'opposition au démarchage, numéro invalide, véhicule hors périmètre, carte grise indisponible ;
- un verdict
eligible= seuil atteint et aucun blocage.
| Critère | Poids par défaut |
|---|---|
| Délai de vente annoncé | 25 |
| Attractivité du véhicule (âge × kilométrage) | 20 |
| Prix espéré vs cote interne | 20 |
| Intérêt relevé en appel | 15 |
| Titularité de la carte grise | 10 |
| État & contrôle technique | 10 |
Le score n'est jamais stocké : il est recalculé à chaque affichage à partir des paramètres courants. Déplacer le seuil dans *Barème & RGPD* requalifie instantanément tout le portefeuille — c'est vérifiable sur les compteurs de la page. Aucun dossier n'est réécrit.
Cote interne
coteVehicule (lib/cote.ts) : prix catalogue neuf → décote annuelle géométrique → correction kilométrique par rapport à 14 000 km/an de référence → coefficient d'état → malus sinistre et CT expiré. Déterministe, arrondie à 50 €. C'est une aide à la qualification, pas une expertise, et l'interface le dit.
L'import de fichier de prospection
Un plateau de téléprospection part d'un fichier acheté ou loué. /import le prend tel qu'il arrive.
Le format n'est pas imposé. lib/csv.ts détecte le séparateur (; d'un export Excel français, ,, tabulation, |), retire le BOM UTF-8, respecte les guillemets doublés, les séparateurs et sauts de ligne enfermés, et les fins de ligne CRLF. detecterMapping rapproche ensuite les en-têtes des champs connus en ignorant casse, accents et ponctuation : Nom de famille, LASTNAME, Téléphone, Mobile, CP ou Code Postal tombent au bon endroit. Ce qui a été mal deviné se corrige à la main.
Le dédoublonnage se fait en deux temps, parce que les deux collisions ne disent pas la même chose : un doublon *dans le fichier* est une erreur du fournisseur, un doublon *contre la base* est un prospect qu'un collègue travaille déjà. Les confondre ferait rappeler la même personne deux fois le même jour. La clé est le numéro ramené à sa forme canonique par lib/telephone.ts : 06 12 34 56 78, +33612345678, 0033612345678 et 612345678 sont le même prospect.
Rien n'est écrit avant confirmation. L'analyse rend un rapport ligne par ligne, chacune portant son sort et son motif en clair — « 0892701234 est un numéro spécial, pas une ligne de particulier », « Même numéro qu'à la ligne 2 ». Les doublons sont revérifiés au moment de l'écriture, la base ayant pu bouger entre-temps.
Un fichier acheté n'a presque jamais de véhicule : la voiture se découvre pendant l'appel, c'est même l'objet de l'appel. Ces fiches entrent avec ANNEE_INCONNUE, une cote affichée — plutôt qu'un montant inventé, les critères véhicule et prix neutralisés au lieu d'être pénalisés au hasard, et un blocage explicite « Véhicule non renseigné » qui interdit la transmission tant que l'agent n'a pas complété. La civilité absente reste null : elle ne se devine pas à partir d'un prénom.
Conformité
- Le consentement RGPD est une case explicite de la disposition d'appel ;
sans lui, la transmission est bloquée quel que soit le score.
- L'opposition au démarchage (type Bloctel) bloque aussi, et ce
comportement est désactivable dans les paramètres — la décision reste à l'exploitant, mais elle est visible.
- Le retraitement Bloctel à 30 jours n'est pas qu'un rappel. La loi impose de
repasser un fichier de démarchage sur la liste d'opposition tous les 30 jours, sous peine d'une amende allant jusqu'à 75 000 €. Chaque fichier importé porte sa date de traitement ; passé l'échéance, fileDAppel retire réellement ses leads de la file du poste d'appel au lieu d'afficher un avertissement. Une jauge encode le temps restant par sa longueur et sa teinte, et passe à l'ambre tant qu'aucun traitement n'a été déclaré : afficher du vert reviendrait à certifier à la place de l'exploitant une conformité que personne n'a attestée.
- L'éligibilité est revérifiée au moment de la transmission, pas seulement à
la qualification : un dossier devenu non conforme depuis est écarté du lot et signalé nommément, le reste passe.
- Les leads en Refus restent en base pour empêcher un rappel ultérieur.
- Aucune donnée personnelle réelle : le jeu de démonstration est un tirage
pseudo-aléatoire à graine fixe (mulberry32, graine 20260831).
Architecture
app/ Routes App Router — toutes dynamiques (lecture disque + cookie de session)
loading.tsx Squelettes de navigation, par route
api/export/leads/ Export CSV (BOM UTF-8, séparateur « ; » pour Excel FR)
api/import/modele/ Modèle de fichier de prospection à télécharger
actions/ Server Actions : leads, lots, campagnes, paramètres, session, import
components/ UI — charts/ (SVG à la main), dialer/, import/, leads/, transmission/, ui/
lib/ types · scoring · cote · seed · store · queries · schemas · format
csv · telephone · import
tests/ 36 tests unitaires + un parcours e2e piloté au navigateur- Next.js 16 (App Router, Turbopack) · React 19 · Bun · TypeScript strict, zéro
any· Tailwind CSS v4 · Zod 4 sur toutes les entrées. - Aucune dépendance UI, de graphiques ni de parsing : les visualisations, la jauge et le lecteur CSV sont écrits ici.
puppeteer-coreest la seule dépendance de développement ajoutée, pour le parcours e2e. - Server Components par défaut. Les composants clients sont ceux qui en ont
besoin : le poste d'appel (chrono, file, formulaire), les formulaires à validation, la courbe d'activité (survol).
- Formulaires contrôlés. React 19 réinitialise un formulaire non contrôlé dès
que son action se termine : une erreur de validation effacerait la saisie de l'agent. lib/use-champs.ts tient l'état de chaque formulaire, la saisie survit à un aller-retour serveur.
- Persistance :
lib/store.ts, fichier JSON, cache invalidé parmtime,
écritures sérialisées et atomiques (fichier temporaire + rename). C'est le seul module à remplacer pour passer sur Postgres — son API (lireBase, muter) est déjà celle d'un dépôt.
- Session : pas d'authentification. L'agent actif est choisi dans l'en-tête
et mémorisé dans un cookie httpOnly (lib/session.ts). Un superviseur voit la file de toute l'équipe, un téléprospecteur seulement la sienne.
Couleurs des graphiques
La palette des visualisations n'a pas été choisie à l'œil : les deux séries de la courbe (#2a78d6 / #eb6834) passent les seuils de séparation en vision déficiente (ΔE 24,7 protanopie, 33,6 vision normale sur fond #fffdf9), la rampe de l'entonnoir est monochrome à luminance monotone, et les sept teintes de statut sont toujours accompagnées de leur libellé — jamais porteuses de l'information seules. Contrastes des badges mesurés entre 5,7:1 et 7,3:1.
Limites assumées
- Pas de téléphonie. Le bouton d'appel ouvre un lien
tel:et le chrono est
manuel. Brancher un softphone (Twilio, Ringover, Aircall) signifie remplacer le bloc chrono/numéro par le SDK et alimenter enregistrerAppel depuis ses webhooks — la forme des données ne bouge pas.
- Pas d'authentification ni de multi-tenant : un seul plateau, un sélecteur
de poste.
- Persistance fichier, adaptée à un poste ou une démo. En multi-instance,
remplacer lib/store.ts.
- Le dédoublonnage ne porte que sur le téléphone. Deux numéros différents
pour la même personne passent encore ; un rapprochement nom + code postal avec arbitrage manuel reste à faire.
- L'import plafonne à 4 Mo et 5 000 lignes par lot : le fichier transite en
entier par le formulaire. Au-delà, il faut le découper — c'est écrit dans l'interface plutôt que découvert à l'usage.
- La cote est un modèle interne simple, pas une cote Argus sous licence.