feat(inscription): email accusé réception, photos enfants, formulaires identité

- Backend: mail après inscription parent avec n° dossier, UPLOAD_PHOTOS_DIR, réponse API
- Frontend: imageBytes + payload photo, utils email/code postal/téléphone, champs dédiés
- Formulaires admin, login (focus), personal_info, child_card, custom_app_text_field

Made-with: Cursor
This commit is contained in:
2026-03-31 00:04:38 +02:00
parent 110240b682
commit bf05b1d7d7
20 changed files with 1148 additions and 273 deletions
+38
View File
@@ -98,6 +98,44 @@ export class MailService {
await this.sendEmail(to, subject, html);
}
/**
* Accusé de réception inscription parent : demande enregistrée + n° de dossier.
* Lutilisateur est en attente de validation ; pas de lien création MDP à ce stade.
*/
async sendParentRegistrationPendingEmail(
to: string,
prenom: string,
nom: string,
numeroDossier: 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 safe = (s: string) =>
(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
const subject = `Votre demande d'inscription sur ${appName} — dossier ${safe(numeroDossier)}`;
const html = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #4CAF50;">Bonjour ${safe(prenom)} ${safe(nom)},</h2>
<p>Nous avons bien enregistré votre demande de création de compte sur <strong>${safe(appName)}</strong>.</p>
<p><strong>Numéro de dossier :</strong> ${safe(numeroDossier)}</p>
<p>Votre dossier est <strong>en attente de validation</strong> par notre équipe. Vous recevrez un email lorsquil aura été traité.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${appUrl}" style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Accéder au site</a>
</div>
<hr style="border: 1px solid #eee; margin: 20px 0;">
<p style="color: #666; font-size: 12px;">Cet email a été envoyé automatiquement. Merci de ne pas y répondre.</p>
</div>
`;
await this.sendEmail(to, subject, html);
}
/**
* Email de refus de dossier avec lien reprise (token).
* Ticket #110 Refus sans suppression
+2
View File
@@ -11,12 +11,14 @@ import { Children } from 'src/entities/children.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { AppConfigModule } from 'src/modules/config';
import { NumeroDossierModule } from 'src/modules/numero-dossier/numero-dossier.module';
import { MailModule } from 'src/modules/mail/mail.module';
@Module({
imports: [
TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]),
forwardRef(() => UserModule),
AppConfigModule,
MailModule,
NumeroDossierModule,
JwtModule.registerAsync({
imports: [ConfigModule],
+27 -1
View File
@@ -28,6 +28,7 @@ import { RepriseIdentifyResponseDto } from './dto/reprise-identify.dto';
import { AppConfigService } from 'src/modules/config/config.service';
import { validateNir } from 'src/common/utils/nir.util';
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
import { MailService } from 'src/modules/mail/mail.service';
@Injectable()
export class AuthService {
@@ -36,6 +37,7 @@ export class AuthService {
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly appConfigService: AppConfigService,
private readonly mailService: MailService,
private readonly numeroDossierService: NumeroDossierService,
@InjectRepository(Parents)
private readonly parentsRepo: Repository<Parents>,
@@ -330,12 +332,34 @@ export class AuthService {
};
});
const numeroDossier = resultat.parent1.numero_dossier ?? '';
try {
await this.mailService.sendParentRegistrationPendingEmail(
resultat.parent1.email,
resultat.parent1.prenom ?? '',
resultat.parent1.nom ?? '',
numeroDossier,
);
if (resultat.parent2) {
await this.mailService.sendParentRegistrationPendingEmail(
resultat.parent2.email,
resultat.parent2.prenom ?? '',
resultat.parent2.nom ?? '',
numeroDossier,
);
}
} catch (err) {
console.error("[inscrireParentComplet] Échec envoi email de confirmation d'inscription", err);
}
return {
message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
parent_id: resultat.parent1.id,
co_parent_id: resultat.parent2?.id,
enfants_ids: resultat.enfants.map(e => e.id),
statut: StatutUtilisateurType.EN_ATTENTE,
numero_dossier: numeroDossier,
};
}
@@ -464,7 +488,9 @@ export class AuthService {
const extension = correspondances[1];
const tamponImage = Buffer.from(correspondances[2], 'base64');
const dossierUpload = '/app/uploads/photos';
const dossierUpload =
(process.env.UPLOAD_PHOTOS_DIR && process.env.UPLOAD_PHOTOS_DIR.trim()) ||
path.join(process.cwd(), 'uploads', 'photos');
await fs.mkdir(dossierUpload, { recursive: true });
const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;