Ajout des documents comptables et correction bug envoi mail nouveau salarie
This commit is contained in:
parent
f27de28bb4
commit
24adff88d5
6 changed files with 242 additions and 134 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
152
lib/autoDeclarationTokenService.ts
Normal file
152
lib/autoDeclarationTokenService.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue