- Add database columns for last_employer_notification_at and last_employee_notification_at in cddu_contracts - Update all email sending endpoints to record timestamps (remind-employer, relance-salarie, docuseal-signature, signature-salarie) - Create smart reminder system with 24h cooldown to prevent spam - Add progress tracking modal with real-time status (pending/sending/success/error) - Display actual employer/employee email addresses in reminder modal - Show notification timestamps in contracts grid with color coding (green/orange/red based on contract start date) - Change employer email button URL from DocuSeal direct link to /signatures-electroniques - Create /api/staff/organizations/emails endpoint for bulk email fetching - Add retroactive migration script for historical email_logs data - Update Contract TypeScript type and API responses to include new fields
250 lines
7.8 KiB
TypeScript
250 lines
7.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService';
|
|
import { ENV } from '@/lib/cleanEnv';
|
|
import { createSbServiceRole } from '@/lib/supabaseServer';
|
|
|
|
/**
|
|
* POST /api/emails/signature-salarie
|
|
*
|
|
* Route API appelée par la Lambda postDocuSealSalarie pour envoyer l'email de signature au salarié
|
|
* Utilise le système universel d'emails v2 avec logging automatique
|
|
*
|
|
* Authentification : API Key dans le header X-API-Key
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
console.log('=== API Email Signature Salarié ===');
|
|
|
|
try {
|
|
// 1. Vérification de l'authentification via API Key
|
|
const apiKey = request.headers.get('X-API-Key');
|
|
const validApiKey = ENV.LAMBDA_API_KEY;
|
|
|
|
if (!validApiKey) {
|
|
console.error('❌ Configuration error: LAMBDA_API_KEY not configured');
|
|
return NextResponse.json(
|
|
{ error: 'Server configuration error' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
if (!apiKey || apiKey !== validApiKey) {
|
|
console.error('❌ Unauthorized: Invalid or missing API key');
|
|
return NextResponse.json(
|
|
{ error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
console.log('✅ Authentication successful');
|
|
|
|
// 2. Récupération et validation des données
|
|
const data = await request.json();
|
|
console.log('📦 Données reçues:', {
|
|
employeeEmail: data.employeeEmail,
|
|
reference: data.reference,
|
|
structure: data.structure,
|
|
firstName: data.firstName,
|
|
matricule: data.matricule
|
|
});
|
|
|
|
const {
|
|
employeeEmail,
|
|
signatureLink,
|
|
reference,
|
|
firstName: providedFirstName,
|
|
organizationName: providedOrgName,
|
|
matricule,
|
|
typecontrat,
|
|
startDate,
|
|
profession,
|
|
productionName,
|
|
organizationId,
|
|
contractId
|
|
} = data;
|
|
|
|
// Validation des champs requis
|
|
if (!employeeEmail || !signatureLink || !reference) {
|
|
console.error('❌ Champs requis manquants');
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Champs manquants',
|
|
required: ['employeeEmail', 'signatureLink', 'reference']
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 3. Récupération du vrai nom de l'organisation depuis Supabase
|
|
let organizationName = providedOrgName || 'Employeur';
|
|
|
|
if (organizationId || contractId) {
|
|
console.log('🔍 Récupération du nom de l\'organisation depuis la base de données...');
|
|
try {
|
|
const supabase = createSbServiceRole();
|
|
|
|
let actualOrgId = organizationId;
|
|
|
|
// Si on n'a que le contractId, récupérer l'org_id depuis le contrat
|
|
if (!actualOrgId && contractId) {
|
|
const { data: contractData } = await supabase
|
|
.from('cddu_contracts')
|
|
.select('org_id')
|
|
.eq('id', contractId)
|
|
.single();
|
|
|
|
if (contractData?.org_id) {
|
|
actualOrgId = contractData.org_id;
|
|
console.log('✅ org_id récupéré depuis le contrat:', actualOrgId);
|
|
}
|
|
}
|
|
|
|
// Récupérer le nom de l'organisation
|
|
if (actualOrgId) {
|
|
const { data: orgData, error: orgError } = await supabase
|
|
.from('organizations')
|
|
.select('name')
|
|
.eq('id', actualOrgId)
|
|
.single();
|
|
|
|
if (!orgError && orgData?.name) {
|
|
organizationName = orgData.name;
|
|
console.log('✅ Nom de l\'organisation trouvé:', organizationName);
|
|
} else {
|
|
console.warn('⚠️ Nom de l\'organisation non trouvé, utilisation du fallback');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('⚠️ Erreur lors de la récupération du nom de l\'organisation:', err);
|
|
}
|
|
}
|
|
|
|
// 4. Récupération du prénom depuis Supabase si non fourni
|
|
let firstName = providedFirstName;
|
|
|
|
if (!firstName && (matricule || employeeEmail)) {
|
|
console.log('🔍 Récupération du prénom depuis la table salaries...');
|
|
try {
|
|
const supabase = createSbServiceRole();
|
|
|
|
// Recherche par matricule ou email
|
|
let query = supabase
|
|
.from('salaries')
|
|
.select('prenom')
|
|
.limit(1);
|
|
|
|
if (organizationId) {
|
|
query = query.eq('employer_id', organizationId);
|
|
}
|
|
|
|
// Priorité au matricule
|
|
if (matricule) {
|
|
query = query.or(`code_salarie.eq.${matricule},num_salarie.eq.${matricule}`);
|
|
} else if (employeeEmail) {
|
|
query = query.eq('adresse_mail', employeeEmail);
|
|
}
|
|
|
|
const { data: salaryData, error: salaryError } = await query;
|
|
|
|
if (!salaryError && salaryData && salaryData[0]?.prenom) {
|
|
firstName = salaryData[0].prenom;
|
|
console.log('✅ Prénom trouvé dans Supabase:', firstName);
|
|
} else {
|
|
console.warn('⚠️ Prénom non trouvé dans Supabase');
|
|
}
|
|
} catch (err) {
|
|
console.error('⚠️ Erreur lors de la récupération du prénom:', err);
|
|
}
|
|
}
|
|
|
|
// 5. Préparation des données de l'email
|
|
const emailData: EmailDataV2 = {
|
|
firstName: firstName || 'Salarié',
|
|
organizationName: organizationName,
|
|
matricule: matricule || 'N/A',
|
|
contractReference: reference,
|
|
typecontrat: typecontrat || 'CDDU',
|
|
startDate: startDate || '',
|
|
profession: profession || '',
|
|
productionName: productionName || '',
|
|
ctaUrl: signatureLink,
|
|
// Ajout des IDs pour le logging (si disponibles)
|
|
organizationId: organizationId,
|
|
contractId: contractId
|
|
};
|
|
|
|
console.log('📧 Préparation de l\'envoi de l\'email:', {
|
|
to: employeeEmail,
|
|
type: 'signature-request-salarie',
|
|
subject: `Signez votre contrat ${organizationName}`,
|
|
firstName: emailData.firstName
|
|
});
|
|
|
|
// 6. Envoi de l'email via le système universel v2
|
|
const messageId = await sendUniversalEmailV2({
|
|
type: 'signature-request-salarie',
|
|
toEmail: employeeEmail,
|
|
data: emailData,
|
|
});
|
|
|
|
console.log('✅ Email envoyé avec succès:', {
|
|
messageId,
|
|
recipient: employeeEmail,
|
|
reference
|
|
});
|
|
|
|
// 7. Mettre à jour le timestamp de dernière notification salarié et stocker le signature_link
|
|
if (contractId) {
|
|
console.log('💾 Mise à jour du contrat dans cddu_contracts...');
|
|
|
|
try {
|
|
const supabase = createSbServiceRole();
|
|
const now = new Date().toISOString();
|
|
|
|
const updateData: any = {
|
|
last_employee_notification_at: now
|
|
};
|
|
|
|
if (signatureLink) {
|
|
updateData.signature_link = signatureLink;
|
|
}
|
|
|
|
const { error: updateError } = await supabase
|
|
.from('cddu_contracts')
|
|
.update(updateData)
|
|
.eq('id', contractId);
|
|
|
|
if (updateError) {
|
|
console.error('⚠️ Erreur lors de la mise à jour:', updateError);
|
|
// Ne pas bloquer le flux, l'email est déjà envoyé
|
|
} else {
|
|
console.log('✅ Timestamp notification salarié enregistré:', now);
|
|
if (signatureLink) {
|
|
console.log('✅ signature_link stocké avec succès');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('⚠️ Exception lors de la mise à jour:', err);
|
|
// Ne pas bloquer le flux
|
|
}
|
|
}
|
|
|
|
// 8. Retour du succès avec le messageId SES
|
|
return NextResponse.json({
|
|
success: true,
|
|
messageId,
|
|
recipient: employeeEmail,
|
|
reference
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('❌ Erreur lors de l\'envoi de l\'email de signature salarié:', error);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Échec de l\'envoi de l\'email',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|