Backend
- migration : champs contact_first_name / contact_last_name (nullable)
sur clients pour personnaliser les variables de relance
- POST /api/v1/plans : création de plan custom avec slug auto-généré
(suffixé en cas de collision, "nouveau"/"new"/"create" réservés)
- POST /api/v1/ai/generate-relance : génération de subject+body via
mistral-small-latest, avec brief utilisateur et tonalité ciblée
- mail_dispatcher : nouvelles variables {{daysLate}}, {{issueDate}},
{{user.fullName}}, {{user.companyName}}, {{client.contactFirstName}},
{{client.contactLastName}} (helper buildRelanceVars exposé pour preview)
- send_relance_job preload désormais l'organization pour exposer son name
Frontend
- /plans/nouveau : wizard 4 étapes (Identité → Cadence → Messages → Récap)
- Stepper en haut, navigation guidée, validation par étape
- Étape 1 : nom + tonalité globale (4 cards Doux/Standard/Ferme/Strict)
avec aperçu de la cadence par défaut associée
- Étape 2 : timeline horizontale (rail rubis-glow + nœuds ◆ teintés
selon la tonalité), édition décalage/ton de l'étape sélectionnée
- Étape 3 : édition par étape avec preview live à droite, chips de
variables cliquables, bouton "Générer avec l'IA" qui ouvre une modale
Mistral (brief + résultat + régénérer)
- Étape 4 : récap avec preview de chaque email rendu sur un client fictif
- Détection des variables sensibles → warning si X clients existants n'ont
pas le champ contactFirstName/contactLastName rempli (UX informative,
fallback vide à l'envoi)
- "Dupliquer" sur chaque card de plan → /plans/nouveau?from=<slug>
pour pré-remplir le wizard à partir d'un plan existant
- ClientCreateDialog : ajout des champs prénom/nom du contact dédié
- TEMPLATE_VARIABLES étendu, helper renderTemplate côté front en miroir
exact de l'implémentation API
- MSW handlers ai/plans/clients alignés sur le nouveau contrat
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import type { Plan, RelanceTone } from "@rubis/shared";
|
|
|
|
/**
|
|
* Mini interpolateur Mustache-like, miroir de
|
|
* `apps/api/app/services/template.ts:renderTemplate`. Utilisé pour la
|
|
* preview live dans le wizard de création de plan custom.
|
|
*/
|
|
export function renderTemplate(template: string, vars: Record<string, unknown>): string {
|
|
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path: string) => {
|
|
const parts = path.split(".");
|
|
let val: unknown = vars;
|
|
for (const p of parts) {
|
|
if (val == null || typeof val !== "object") return "";
|
|
val = (val as Record<string, unknown>)[p];
|
|
}
|
|
return val == null ? "" : String(val);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Vars de preview : utilisées par le wizard pour montrer l'email tel
|
|
* qu'il sera reçu, avec un client/facture fictifs.
|
|
*/
|
|
export const PREVIEW_VARS = {
|
|
client: {
|
|
name: "Boulangerie Martin SARL",
|
|
email: "compta@boulangerie-martin.fr",
|
|
contactFirstName: "Marie",
|
|
contactLastName: "Martin",
|
|
},
|
|
user: {
|
|
fullName: "Arthur Barré",
|
|
companyName: "Maçonnerie Dupont",
|
|
},
|
|
numero: "F-2026-0042",
|
|
amount: "1 240,00 €",
|
|
dueDate: "15/04/2026",
|
|
issueDate: "15/03/2026",
|
|
daysLate: "12",
|
|
signature: "Cordialement,\nArthur Barré\nMaçonnerie Dupont",
|
|
} as const;
|
|
|
|
/**
|
|
* Helpers de présentation des plans de relance.
|
|
* Garde la conversion tonalité → label public au même endroit.
|
|
*/
|
|
|
|
/** Label visible utilisateur pour chaque ton (cf. wireframe 3.1, chips). */
|
|
export const TONE_LABELS: Record<RelanceTone, string> = {
|
|
amical: "Amical",
|
|
courtois: "Standard",
|
|
ferme: "Ferme",
|
|
mise_en_demeure: "Mise en demeure",
|
|
};
|
|
|
|
/** Tonalité globale d'un plan = la dernière étape (la plus stricte). */
|
|
export function planOverallTone(plan: Pick<Plan, "steps">): RelanceTone {
|
|
const last = plan.steps[plan.steps.length - 1];
|
|
return last?.tone ?? "courtois";
|
|
}
|
|
|
|
/** Label court d'humeur d'un plan ("Doux" / "Standard" / "Ferme" / "Strict"). */
|
|
export function planMoodLabel(plan: Pick<Plan, "steps">): string {
|
|
const tone = planOverallTone(plan);
|
|
switch (tone) {
|
|
case "amical":
|
|
return "Doux";
|
|
case "courtois":
|
|
return "Standard";
|
|
case "ferme":
|
|
return "Ferme";
|
|
case "mise_en_demeure":
|
|
return "Strict";
|
|
default:
|
|
return "Standard";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Variables de template disponibles dans les emails de relance.
|
|
* Les chips dans l'éditeur viennent piocher ici.
|
|
*/
|
|
export type TemplateVariable = {
|
|
/** Token inséré tel quel dans le body (ex. "{{numero}}"). */
|
|
token: string;
|
|
/** Label affiché sur le chip cliquable. */
|
|
label: string;
|
|
/** Aperçu utilisé dans l'éditeur (placeholder réaliste). */
|
|
preview: string;
|
|
/** Si présent, exige qu'un champ correspondant soit rempli sur la fiche
|
|
* client pour fonctionner. Sinon le token est interpolé en chaîne vide. */
|
|
requiresClientField?: "contactFirstName" | "contactLastName";
|
|
};
|
|
|
|
export const TEMPLATE_VARIABLES: TemplateVariable[] = [
|
|
// Client
|
|
{ token: "{{client.name}}", label: "Raison sociale", preview: "Boulangerie Martin SARL" },
|
|
{
|
|
token: "{{client.contactFirstName}}",
|
|
label: "Prénom contact",
|
|
preview: "Marie",
|
|
requiresClientField: "contactFirstName",
|
|
},
|
|
{
|
|
token: "{{client.contactLastName}}",
|
|
label: "Nom contact",
|
|
preview: "Martin",
|
|
requiresClientField: "contactLastName",
|
|
},
|
|
// Facture
|
|
{ token: "{{numero}}", label: "Numéro facture", preview: "F-2026-0042" },
|
|
{ token: "{{amount}}", label: "Montant TTC", preview: "1 240,00 €" },
|
|
{ token: "{{dueDate}}", label: "Échéance", preview: "15/04/2026" },
|
|
{ token: "{{issueDate}}", label: "Date émission", preview: "15/03/2026" },
|
|
{ token: "{{daysLate}}", label: "Jours de retard", preview: "12" },
|
|
// Expéditeur (la TPE qui envoie)
|
|
{ token: "{{user.fullName}}", label: "Votre nom", preview: "Arthur Barré" },
|
|
{ token: "{{user.companyName}}", label: "Votre entreprise", preview: "Maçonnerie Dupont" },
|
|
{ token: "{{signature}}", label: "Signature", preview: "Cordialement,\nArthur" },
|
|
];
|
|
|
|
/**
|
|
* Variables qui exigent qu'un champ correspondant soit rempli sur la fiche
|
|
* client. Pour chaque token utilisé dans un template, on peut détecter
|
|
* combien de clients existants n'ont pas le champ requis (warning UX).
|
|
*/
|
|
export type ClientRequiredField = "contactFirstName" | "contactLastName";
|