Ajout des documents comptables et correction bug envoi mail nouveau salarie

This commit is contained in:
Renaud 2025-10-12 20:06:31 +02:00
parent f27de28bb4
commit 24adff88d5
6 changed files with 242 additions and 134 deletions

View file

@ -1,11 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { createSbServiceRole } from '@/lib/supabaseServer';
import crypto from 'crypto';
import { sendUniversalEmailV2 } from '@/lib/emailTemplateService';
import { generateAutoDeclarationToken } from '@/lib/autoDeclarationTokenService';
export async function POST(request: NextRequest) {
try {
const { salarie_id } = await request.json();
const { salarie_id, send_email = true } = await request.json();
if (!salarie_id) {
return NextResponse.json(
@ -14,114 +12,36 @@ export async function POST(request: NextRequest) {
);
}
const supabase = createSbServiceRole();
const result = await generateAutoDeclarationToken({
salarie_id,
send_email
});
// Récupérer les informations du salarié
const { data: salarie, error: salarieError } = await supabase
.from('salaries')
.select('id, nom, prenom, adresse_mail, code_salarie, civilite, employer_id')
.eq('id', salarie_id)
.single();
if (salarieError || !salarie) {
console.error('Erreur récupération salarié:', salarieError);
if (!result.success) {
const status = result.error === 'Salarié non trouvé' ? 404 :
result.error === 'Email du salarié requis' ? 400 : 500;
return NextResponse.json(
{ error: 'Salarié non trouvé' },
{ status: 404 }
{ error: result.error },
{ status }
);
}
// Récupérer les informations de l'employeur séparément
let organizationName = 'votre employeur';
if (salarie.employer_id) {
const { data: organization, error: orgError } = await supabase
.from('organizations')
.select('name')
.eq('id', salarie.employer_id)
.single();
console.log('🏢 [TOKEN] Organization query result:', { organization, orgError, employer_id: salarie.employer_id });
if (organization?.name) {
organizationName = organization.name;
console.log('✅ [TOKEN] Organization name found:', organizationName);
} else {
console.log('⚠️ [TOKEN] No organization name found, using default');
}
} else {
console.log('⚠️ [TOKEN] No employer_id found for salarie');
}
if (!salarie.adresse_mail) {
// Si le token a été créé mais que l'email n'a pas pu être envoyé
if (result.email_sent === false && result.error) {
return NextResponse.json(
{ error: 'Email du salarié requis' },
{ status: 400 }
);
}
// Supprimer d'éventuels tokens existants
await supabase
.from('auto_declaration_tokens')
.delete()
.eq('salarie_id', salarie_id);
// Générer un nouveau token sécurisé
const token = crypto.randomBytes(32).toString('hex');
// Créer le token en base avec expiration dans 72h
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 72);
const { error: tokenError } = await supabase
.from('auto_declaration_tokens')
.insert({
token,
salarie_id: salarie_id,
expires_at: expiresAt.toISOString(),
used: false
});
if (tokenError) {
console.error('Erreur création token:', tokenError);
return NextResponse.json(
{ error: 'Erreur lors de la génération du token' },
{ status: 500 }
);
}
// Préparer l'URL sécurisée pour l'auto-déclaration
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const autoDeclarationUrl = `${baseUrl}/auto-declaration?token=${token}`;
// Envoyer l'email d'invitation
try {
const emailResult = await sendUniversalEmailV2({
type: 'auto-declaration-invitation',
toEmail: salarie.adresse_mail,
data: {
firstName: salarie.prenom || 'Cher collaborateur',
organizationName: organizationName, // Utilisé à la fois dans le texte ET dans la card
matricule: salarie.code_salarie || 'Non défini',
ctaUrl: autoDeclarationUrl
}
});
return NextResponse.json({
success: true,
message: 'Token généré et invitation envoyée avec succès',
token: token, // Pour debug uniquement, retirer en production
email_sent: true,
messageId: emailResult
});
} catch (emailError) {
console.error('Erreur envoi email invitation:', emailError);
return NextResponse.json(
{ error: 'Token créé mais erreur lors de l\'envoi de l\'email' },
{ error: result.error },
{ status: 207 }
);
}
return NextResponse.json({
success: true,
message: 'Token généré et invitation envoyée avec succès',
token: result.token, // Pour debug uniquement, retirer en production
email_sent: result.email_sent,
messageId: result.messageId
});
} catch (error) {
console.error('Erreur génération token:', error);
return NextResponse.json(

View file

@ -3,6 +3,7 @@ export const dynamic = "force-dynamic";
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { getS3SignedUrl } from "@/lib/aws-s3";
function json(status: number, body: any) {
return NextResponse.json(body, { status });
@ -97,19 +98,36 @@ export async function GET(req: Request) {
console.log('📄 Documents API - Found documents:', documents?.length || 0);
// 5) Transformer les documents au format attendé par le frontend
const formattedDocuments = (documents || []).map(doc => ({
id: doc.id,
title: doc.filename || doc.type_label || 'Document',
url: doc.storage_path, // L'URL S3 présignée sera générée côté client si nécessaire
updatedAt: doc.date_added,
sizeBytes: doc.size_bytes || 0,
period_label: doc.period_label,
meta: {
category: doc.category,
type_label: doc.type_label,
}
}));
// 5) Transformer les documents au format attendé par le frontend avec URLs S3 présignées
const formattedDocuments = await Promise.all(
(documents || []).map(async (doc) => {
let presignedUrl: string | null = null;
// Générer l'URL S3 présignée si storage_path existe
if (doc.storage_path) {
try {
presignedUrl = await getS3SignedUrl(doc.storage_path, 3600); // Expire dans 1 heure
console.log('✅ Generated presigned URL for:', doc.filename);
} catch (error) {
console.error('❌ Error generating presigned URL for:', doc.filename, error);
}
}
return {
id: doc.id,
title: doc.filename || doc.type_label || 'Document',
url: presignedUrl, // URL S3 présignée prête à l'emploi
updatedAt: doc.date_added,
sizeBytes: doc.size_bytes || 0,
period_label: doc.period_label,
meta: {
category: doc.category,
type_label: doc.type_label,
storage_path: doc.storage_path, // Garder le path original pour référence
}
};
})
);
console.log('📄 Documents API - Returning formatted documents:', formattedDocuments.length);

View file

@ -4,6 +4,7 @@ import { NextResponse, NextRequest } from "next/server";
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
import { resolveActiveOrg } from "@/lib/resolveActiveOrg";
import { sendUniversalEmailV2 } from "@/lib/emailTemplateService";
import { generateAutoDeclarationToken } from "@/lib/autoDeclarationTokenService";
type SalarieRow = {
matricule: string;
@ -182,6 +183,16 @@ export async function POST(req: NextRequest) {
let computedNum: number | null = null;
if (orgId) {
try {
// Récupérer le code employeur depuis organization_details
const { data: orgDetailsData } = await supabase
.from('organization_details')
.select('code_employeur')
.eq('organization_id', orgId)
.single();
const codeEmployeur = orgDetailsData?.code_employeur || '';
console.log('🔍 [MATRICULE] orgId:', orgId, 'code_employeur:', codeEmployeur);
const { data: rows, error: qerr } = await supabase
.from('salaries')
.select('code_salarie')
@ -190,6 +201,7 @@ export async function POST(req: NextRequest) {
.limit(1000);
if (!qerr && Array.isArray(rows)) {
console.log('🔍 [MATRICULE] Nombre de salariés existants:', rows.length);
let maxNum = -Infinity;
let maxPrefix = '';
let maxPad = 0;
@ -211,13 +223,16 @@ export async function POST(req: NextRequest) {
const padded = String(nextNum).padStart(maxPad, '0');
computedCode = `${maxPrefix}${padded}`;
computedNum = nextNum;
console.log('✅ [MATRICULE] Incrémentation:', computedCode);
} else {
// No existing numeric matricules found: fallback to simple numbering
// No existing numeric matricules found: use code_employeur as prefix
computedNum = 1;
computedCode = `001`;
computedCode = codeEmployeur ? `${codeEmployeur}001` : `001`;
console.log('✅ [MATRICULE] Premier salarié, code généré:', computedCode, '(codeEmployeur:', codeEmployeur, ')');
}
}
} catch (e) {
console.error('❌ [MATRICULE] Erreur génération:', e);
// ignore computation errors and continue without code
}
}
@ -279,10 +294,20 @@ export async function POST(req: NextRequest) {
}
}
const orgDetails = orgId ? await supabase.from('organizations').select('name, code_employeur').eq('id', orgId).single() : { data: null };
// Récupérer les infos de l'organisation depuis les deux tables
const orgData = orgId ? await supabase.from('organizations').select('name').eq('id', orgId).single() : { data: null };
const orgDetailsData = orgId ? await supabase.from('organization_details').select('code_employeur').eq('organization_id', orgId).single() : { data: null };
const orgDetails = {
data: orgData?.data ? {
name: orgData.data.name,
code_employeur: orgDetailsData?.data?.code_employeur || null
} : null
};
// 1. Email de notification à l'équipe (existant)
if (user && orgDetails?.data) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
await sendUniversalEmailV2({
type: 'employee-created',
toEmail: user.email || 'paie@odentas.fr', // Fallback
@ -293,7 +318,7 @@ export async function POST(req: NextRequest) {
employeeName: data.salarie,
email: data.adresse_mail,
matricule: data.code_salarie,
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/salaries/${data.id}`,
ctaUrl: `${baseUrl}/salaries/${data.id}`,
}
});
}
@ -301,22 +326,15 @@ export async function POST(req: NextRequest) {
// 2. Générer token et envoyer invitation au salarié (nouveau)
if (data.adresse_mail) {
try {
const tokenResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auto-declaration/generate-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
salarie_id: data.id,
send_email: true
})
const result = await generateAutoDeclarationToken({
salarie_id: data.id,
send_email: true
});
if (tokenResponse.ok) {
if (result.success) {
console.log('✅ [API /salaries POST] Token généré et invitation envoyée au salarié');
} else {
const errorText = await tokenResponse.text();
console.error('❌ [API /salaries POST] Erreur génération token:', errorText);
console.error('❌ [API /salaries POST] Erreur génération token:', result.error);
}
} catch (tokenError) {
console.error('❌ [API /salaries POST] Erreur lors de la génération du token:', tokenError);

View file

@ -0,0 +1,152 @@
import { createSbServiceRole } from '@/lib/supabaseServer';
import crypto from 'crypto';
import { sendUniversalEmailV2 } from '@/lib/emailTemplateService';
interface GenerateTokenOptions {
salarie_id: string;
send_email?: boolean;
}
interface GenerateTokenResult {
success: boolean;
token?: string;
messageId?: string;
email_sent?: boolean;
error?: string;
}
/**
* Génère un token d'auto-déclaration pour un salarié et optionnellement envoie l'email d'invitation
*/
export async function generateAutoDeclarationToken(
options: GenerateTokenOptions
): Promise<GenerateTokenResult> {
const { salarie_id, send_email = true } = options;
try {
const supabase = createSbServiceRole();
// Récupérer les informations du salarié
const { data: salarie, error: salarieError } = await supabase
.from('salaries')
.select('id, nom, prenom, adresse_mail, code_salarie, civilite, employer_id')
.eq('id', salarie_id)
.single();
if (salarieError || !salarie) {
console.error('Erreur récupération salarié:', salarieError);
return {
success: false,
error: 'Salarié non trouvé'
};
}
// Récupérer les informations de l'employeur séparément
let organizationName = 'votre employeur';
if (salarie.employer_id) {
const { data: organization, error: orgError } = await supabase
.from('organizations')
.select('name')
.eq('id', salarie.employer_id)
.single();
console.log('🏢 [TOKEN] Organization query result:', { organization, orgError, employer_id: salarie.employer_id });
if (organization?.name) {
organizationName = organization.name;
console.log('✅ [TOKEN] Organization name found:', organizationName);
} else {
console.log('⚠️ [TOKEN] No organization name found, using default');
}
} else {
console.log('⚠️ [TOKEN] No employer_id found for salarie');
}
if (!salarie.adresse_mail) {
return {
success: false,
error: 'Email du salarié requis'
};
}
// Supprimer d'éventuels tokens existants
await supabase
.from('auto_declaration_tokens')
.delete()
.eq('salarie_id', salarie_id);
// Générer un nouveau token sécurisé
const token = crypto.randomBytes(32).toString('hex');
// Créer le token en base avec expiration dans 7 jours
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
const { error: tokenError } = await supabase
.from('auto_declaration_tokens')
.insert({
token,
salarie_id: salarie_id,
expires_at: expiresAt.toISOString(),
used: false
});
if (tokenError) {
console.error('Erreur création token:', tokenError);
return {
success: false,
error: 'Erreur lors de la génération du token'
};
}
// Si on ne doit pas envoyer d'email, on s'arrête là
if (!send_email) {
return {
success: true,
token,
email_sent: false
};
}
// Préparer l'URL sécurisée pour l'auto-déclaration
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
const autoDeclarationUrl = `${baseUrl}/auto-declaration?token=${token}`;
// Envoyer l'email d'invitation
try {
const emailResult = await sendUniversalEmailV2({
type: 'auto-declaration-invitation',
toEmail: salarie.adresse_mail,
data: {
firstName: salarie.prenom || 'Cher collaborateur',
organizationName: organizationName,
matricule: salarie.code_salarie || 'Non défini',
ctaUrl: autoDeclarationUrl
}
});
return {
success: true,
token,
email_sent: true,
messageId: emailResult
};
} catch (emailError) {
console.error('Erreur envoi email invitation:', emailError);
return {
success: true,
token,
email_sent: false,
error: 'Token créé mais erreur lors de l\'envoi de l\'email'
};
}
} catch (error) {
console.error('Erreur génération token:', error);
return {
success: false,
error: 'Erreur serveur'
};
}
}

View file

@ -496,9 +496,9 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
title: 'Complétez votre dossier d\'embauche',
greeting: '{{#if firstName}}Bonjour {{firstName}},{{/if}}',
mainMessage: 'Dans le cadre de votre projet d\'embauche géré par les services d\'Odentas pour le compte de {{organizationName}}, nous vous invitons à compléter votre dossier.\n\nPour finaliser votre embauche, nous avons besoin que vous nous transmettiez certaines informations personnelles et justificatifs.',
closingMessage: 'Important : Ce lien d\'accès expire dans 72 heures. Pensez à compléter votre dossier avant cette date.<br><br>Besoin d\'aide ? N\'hésitez pas à nous contacter à <a href="mailto:paie@odentas.fr" style="color:#0B5FFF;">paie@odentas.fr</a> pour toute question.',
closingMessage: 'Important : Ce lien d\'accès expire dans 7 jours. Pensez à compléter votre dossier avant cette date.<br><br>Besoin d\'aide ? N\'hésitez pas à nous contacter à <a href="mailto:paie@odentas.fr" style="color:#0B5FFF;">paie@odentas.fr</a> pour toute question.',
ctaText: 'Compléter mon dossier',
footerText: 'Ce lien est personnel et sécurisé. Ne le partagez avec personne d\'autre.',
footerText: 'Ce lien est personnel et sécurisé. Ne le partagez avec personne d\'autre.<br><br>Vous recevez cet e-mail car votre employeur ou futur employeur est client de Odentas, pour vous notifier d\'une action sur votre contrat de travail ou votre projet d\'embauche avec cet employeur. Ce mail ne constitue pas une promesse d\'embauche.',
preheaderText: 'Dossier d\'embauche · Complétez vos informations',
colors: {
headerColor: STANDARD_COLORS.HEADER,

View file

@ -119,7 +119,7 @@
{{/if}}
<div class="footer" style="font-size:12px; color:#64748B; text-align:center; padding:18px 28px 28px 28px; line-height:1.4;">
<p>{{{footerText}}}<br><span class="muted" style="color:#94A3B8;">© 2021-2025 Odentas Media SAS</span></p>
<p>{{{footerText}}}<br><span class="muted" style="color:#94A3B8;">© 2021-2025 Odentas Media SAS | RCS Paris 907880348 | 6 rue d'Armaillé, 75017 Paris</span></p>
</div>
</div>
</div>