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>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
import { Exception } from '@adonisjs/core/exceptions'
|
|
import type { HttpContext } from '@adonisjs/core/http'
|
|
import { generateRelance } from '#services/ai_relance_generator'
|
|
|
|
const RELANCE_TONES = ['amical', 'courtois', 'ferme', 'mise_en_demeure'] as const
|
|
|
|
const generateRelanceValidator = vine.create({
|
|
tone: vine.enum(RELANCE_TONES),
|
|
offsetDays: vine.number().min(-30).max(180),
|
|
// Brief libre. On accepte vide : Mistral génère alors un message standard
|
|
// pour la tonalité + timing donnés.
|
|
prompt: vine.string().maxLength(1000).optional(),
|
|
planContext: vine.string().maxLength(500).optional(),
|
|
})
|
|
|
|
/**
|
|
* Endpoints IA. V1 : uniquement génération de templates de relance pour le
|
|
* wizard de création de plan custom. Mistral est déjà utilisé pour l'OCR
|
|
* (cf. mistral_ocr_provider.ts) — on réutilise la même clé API.
|
|
*/
|
|
export default class AiController {
|
|
/**
|
|
* POST /ai/generate-relance
|
|
*
|
|
* Génère subject + body avec des placeholders Mustache prêts à insérer.
|
|
* L'utilisateur peut régénérer pour avoir une variante.
|
|
*/
|
|
async generateRelance({ auth, request, response }: HttpContext) {
|
|
auth.getUserOrFail() // auth requise
|
|
const payload = await request.validateUsing(generateRelanceValidator)
|
|
|
|
try {
|
|
const result = await generateRelance({
|
|
tone: payload.tone,
|
|
offsetDays: payload.offsetDays,
|
|
prompt: payload.prompt ?? '',
|
|
planContext: payload.planContext,
|
|
})
|
|
return response.json({ data: result })
|
|
} catch (err) {
|
|
// On wrap pour passer par le handler global et garder le format
|
|
// d'erreur uniforme côté front.
|
|
throw new Exception(
|
|
err instanceof Error ? err.message : 'Génération IA indisponible',
|
|
{ status: 502, code: 'ai_generation_failed' }
|
|
)
|
|
}
|
|
}
|
|
}
|