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>
42 lines
1.5 KiB
TypeScript
42 lines
1.5 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
|
|
const name = () => vine.string().minLength(2).maxLength(120)
|
|
const email = () => vine.string().email().maxLength(254)
|
|
// SIRET = 14 chiffres exactement (cf. INSEE).
|
|
const siret = () => vine.string().regex(/^\d{14}$/)
|
|
const phone = () => vine.string().maxLength(40)
|
|
const address = () => vine.string().maxLength(500)
|
|
const notes = () => vine.string().maxLength(2000)
|
|
// Prénom/nom du contact dédié — utilisés comme variables dans les templates
|
|
// custom ({{client.contactFirstName}}). Optionnels.
|
|
const contactName = () => vine.string().minLength(1).maxLength(80)
|
|
|
|
/**
|
|
* Validator pour POST /clients. Email **requis** : sans email, Rubis ne
|
|
* peut pas relancer (pivot produit, cf. CLAUDE.md → Principes).
|
|
*/
|
|
export const createClientValidator = vine.create({
|
|
name: name(),
|
|
email: email(),
|
|
contactFirstName: contactName().nullable().optional(),
|
|
contactLastName: contactName().nullable().optional(),
|
|
phone: phone().nullable().optional(),
|
|
address: address().nullable().optional(),
|
|
siret: siret().nullable().optional(),
|
|
notes: notes().nullable().optional(),
|
|
})
|
|
|
|
/**
|
|
* Validator pour PATCH /clients/:id. Tous les champs optionnels.
|
|
*/
|
|
export const updateClientValidator = vine.create({
|
|
name: name().optional(),
|
|
email: email().optional(),
|
|
contactFirstName: contactName().nullable().optional(),
|
|
contactLastName: contactName().nullable().optional(),
|
|
phone: phone().nullable().optional(),
|
|
address: address().nullable().optional(),
|
|
siret: siret().nullable().optional(),
|
|
notes: notes().nullable().optional(),
|
|
})
|