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>
37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
|
|
const RELANCE_TONES = ['amical', 'courtois', 'ferme', 'mise_en_demeure'] as const
|
|
|
|
const planStep = vine.object({
|
|
// id optionnel : présent si on édite une étape existante, absent pour
|
|
// une création (le contrôleur le générera).
|
|
id: vine.string().optional(),
|
|
order: vine.number().min(0),
|
|
// Plage : -30 (rappel avant échéance) à 180 jours (gros retards).
|
|
offsetDays: vine.number().min(-30).max(180),
|
|
tone: vine.enum(RELANCE_TONES),
|
|
subject: vine.string().minLength(1).maxLength(200),
|
|
body: vine.string().minLength(1).maxLength(5000),
|
|
requiresManualValidation: vine.boolean(),
|
|
})
|
|
|
|
/**
|
|
* Validator pour PATCH /plans/:slug. Tous les champs optionnels — l'éditeur
|
|
* front peut envoyer juste `name` ou juste `steps` selon ce qu'il modifie.
|
|
*/
|
|
export const updatePlanValidator = vine.create({
|
|
name: vine.string().minLength(1).maxLength(80).optional(),
|
|
description: vine.string().maxLength(500).optional(),
|
|
steps: vine.array(planStep).minLength(1).maxLength(10).optional(),
|
|
})
|
|
|
|
/**
|
|
* Validator pour POST /plans — création d'un plan custom.
|
|
* Le slug est généré côté contrôleur depuis le name.
|
|
*/
|
|
export const createPlanValidator = vine.create({
|
|
name: vine.string().minLength(1).maxLength(80),
|
|
description: vine.string().maxLength(500).optional(),
|
|
steps: vine.array(planStep).minLength(1).maxLength(10),
|
|
})
|