feat(#110): refus dossier propagé aux co-parents + mail aligné
- `refuseUser` charge tous les parents du même numéro de dossier, leur applique le même token_reprise (7 jours), trace une validation refus par compte et envoie un mail de refus à chacun. Le refus devient une action dossier : peu importe quel parent ouvre ensuite le lien. - Échec d'envoi mail loggé sans bloquer les autres destinataires. - Mail de refus aligné sur le look des autres templates (titre/bouton verts, bloc motif gestionnaire en encart, échappement HTML cohérent, appUrl normalisée). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b6e83bf9ef
commit
9eb62ddd13
@ -214,21 +214,29 @@ export class MailService {
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
const appName = this.configService.get<string>('app_name', "P'titsPas");
|
||||
const appUrl = this.configService.get<string>('app_url', 'https://app.ptits-pas.fr');
|
||||
const appUrl = (this.configService.get<string>('app_url', 'https://app.ptits-pas.fr') || '').replace(/\/+$/, '');
|
||||
const repriseLink = `${appUrl}/reprise?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const safe = (s: string) =>
|
||||
(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
const subject = `Votre dossier – compléments demandés`;
|
||||
const commentBlock = comment
|
||||
? `<p><strong>Message du gestionnaire :</strong></p><p>${comment.replace(/</g, '<').replace(/>/g, '>')}</p>`
|
||||
: '';
|
||||
const commentText = safe(comment || 'Le gestionnaire vous demande de compléter votre dossier avant une nouvelle validation.');
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #333;">Bonjour ${prenom} ${nom},</h2>
|
||||
<p>Votre dossier d'inscription sur <strong>${appName}</strong> n'a pas pu être validé en l'état.</p>
|
||||
${commentBlock}
|
||||
<h2 style="color: #4CAF50;">Bonjour ${safe(prenom)} ${safe(nom)},</h2>
|
||||
<p>Votre dossier d'inscription sur <strong>${safe(appName)}</strong> n'a pas pu être validé en l'état.</p>
|
||||
<div style="background-color: #F4FBF5; border-left: 4px solid #4CAF50; padding: 12px 16px; margin: 20px 0;">
|
||||
<p style="margin: 0 0 8px 0;"><strong>Message du gestionnaire :</strong></p>
|
||||
<p style="margin: 0;">${commentText}</p>
|
||||
</div>
|
||||
<p>Vous pouvez corriger les éléments indiqués et soumettre à nouveau votre dossier en cliquant sur le lien ci-dessous.</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${repriseLink}" style="background-color: #2196F3; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Reprendre mon dossier</a>
|
||||
<a href="${repriseLink}" style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Reprendre mon dossier</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 12px;">Ce lien est valable 7 jours. Si vous n'avez pas demandé cette reprise, vous pouvez ignorer cet email.</p>
|
||||
<hr style="border: 1px solid #eee; margin: 20px 0;">
|
||||
|
||||
@ -332,7 +332,7 @@ export class UserService {
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
/** Refuser un compte (en_attente -> refuse) ; tracé validations, token reprise, email. Ticket #110 */
|
||||
/** Refuser un dossier (en_attente -> refuse) ; tracé validations, token reprise partagé, emails. Ticket #110 */
|
||||
async refuseUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
|
||||
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
|
||||
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
|
||||
@ -340,27 +340,57 @@ export class UserService {
|
||||
const user = await this.usersRepository.findOne({ where: { id: user_id } });
|
||||
if (!user) throw new NotFoundException('Utilisateur introuvable');
|
||||
if (user.statut !== StatutUtilisateurType.EN_ATTENTE) {
|
||||
throw new BadRequestException('Seul un compte en attente peut être refusé.');
|
||||
throw new BadRequestException('Seul un dossier en attente peut être refusé.');
|
||||
}
|
||||
|
||||
const tokenReprise = crypto.randomUUID();
|
||||
const expireLe = new Date();
|
||||
expireLe.setDate(expireLe.getDate() + 7);
|
||||
|
||||
user.statut = StatutUtilisateurType.REFUSE;
|
||||
user.token_reprise = tokenReprise;
|
||||
user.token_reprise_expire_le = expireLe;
|
||||
const savedUser = await this.usersRepository.save(user);
|
||||
const usersDossier = user.role === RoleType.PARENT && user.numero_dossier
|
||||
? await this.usersRepository.find({
|
||||
where: {
|
||||
role: RoleType.PARENT,
|
||||
numero_dossier: user.numero_dossier,
|
||||
},
|
||||
})
|
||||
: [user];
|
||||
|
||||
const validation = this.validationRepository.create({
|
||||
const usersARefuser = usersDossier.length ? usersDossier : [user];
|
||||
const utilisateurInvalide = usersARefuser.find(
|
||||
(u) => u.statut !== StatutUtilisateurType.EN_ATTENTE,
|
||||
);
|
||||
if (utilisateurInvalide) {
|
||||
throw new BadRequestException(
|
||||
'Tous les comptes du dossier doivent être en attente pour refuser le dossier.',
|
||||
);
|
||||
}
|
||||
|
||||
const savedUsers = await this.usersRepository.manager.transaction(async (manager) => {
|
||||
const updatedUsers: Users[] = [];
|
||||
|
||||
for (const userARefuser of usersARefuser) {
|
||||
userARefuser.statut = StatutUtilisateurType.REFUSE;
|
||||
userARefuser.token_reprise = tokenReprise;
|
||||
userARefuser.token_reprise_expire_le = expireLe;
|
||||
|
||||
const savedUser = await manager.save(Users, userARefuser);
|
||||
updatedUsers.push(savedUser);
|
||||
|
||||
const validation = manager.create(Validation, {
|
||||
user: savedUser,
|
||||
type: 'refus_compte',
|
||||
status: StatutValidationType.REFUSE,
|
||||
validated_by: currentUser,
|
||||
comment,
|
||||
});
|
||||
await this.validationRepository.save(validation);
|
||||
await manager.save(Validation, validation);
|
||||
}
|
||||
|
||||
return updatedUsers;
|
||||
});
|
||||
|
||||
for (const savedUser of savedUsers) {
|
||||
try {
|
||||
await this.mailService.sendRefusEmail(
|
||||
savedUser.email,
|
||||
@ -372,8 +402,9 @@ export class UserService {
|
||||
} catch (err) {
|
||||
this.logger.warn(`Envoi email refus échoué pour ${savedUser.email}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return savedUser;
|
||||
return savedUsers.find((savedUser) => savedUser.id === user.id) ?? savedUsers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user