✨ Nouvelles fonctionnalités - Page de gestion des avenants (/staff/avenants) - Page de détail d'un avenant (/staff/avenants/[id]) - Création d'avenants (objet, durée, rémunération) - Génération automatique de PDF d'avenant - Signature électronique via DocuSeal (employeur puis salarié) - Changement manuel du statut d'un avenant - Suppression d'avenants 🔧 Routes API - POST /api/staff/amendments/create - Créer un avenant - POST /api/staff/amendments/generate-pdf - Générer le PDF - POST /api/staff/amendments/[id]/send-signature - Envoyer en signature - POST /api/staff/amendments/[id]/change-status - Changer le statut - POST /api/webhooks/docuseal-amendment - Webhook après signature employeur - GET /api/signatures-electroniques/avenants - Liste des avenants en signature 📧 Système email universel v2 - Migration vers le système universel v2 pour les emails d'avenants - Template 'signature-request-employee-amendment' pour salariés - Insertion automatique dans DynamoDB pour la Lambda - Mise à jour automatique du statut dans Supabase 🗄️ Base de données - Table 'avenants' avec tous les champs (objet, durée, rémunération) - Colonnes de notification (last_employer_notification_at, last_employee_notification_at) - Liaison avec cddu_contracts 🎨 Composants - AvenantDetailPageClient - Détail complet d'un avenant - ChangeStatusModal - Changement de statut manuel - SendSignatureModal - Envoi en signature - DeleteAvenantModal - Suppression avec confirmation - AvenantSuccessModal - Confirmation de création 📚 Documentation - AVENANT_EMAIL_SYSTEM_MIGRATION.md - Guide complet de migration 🐛 Corrections - Fix parsing défensif dans Lambda AWS - Fix récupération des données depuis DynamoDB - Fix statut MFA !== 'verified' au lieu de === 'unverified'
324 lines
12 KiB
TypeScript
324 lines
12 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||
import { cookies } from "next/headers";
|
||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
|
||
import axios from "axios";
|
||
import { sendUniversalEmailV2 } from "@/lib/emailTemplateService";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
export async function POST(
|
||
request: NextRequest,
|
||
{ params }: { params: Promise<{ id: string }> }
|
||
) {
|
||
try {
|
||
const { id } = await params;
|
||
console.log("🔵 [SEND SIGNATURE] Début de l'envoi en signature pour avenant:", id);
|
||
|
||
const supabase = createRouteHandlerClient({ cookies });
|
||
|
||
// Vérifier l'authentification
|
||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||
if (authError || !user) {
|
||
console.error("❌ [SEND SIGNATURE] Erreur authentification:", authError);
|
||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||
}
|
||
console.log("✅ [SEND SIGNATURE] Utilisateur authentifié:", user.id);
|
||
|
||
// Vérifier que l'utilisateur est staff
|
||
const { data: staffUser } = await supabase
|
||
.from("staff_users")
|
||
.select("is_staff")
|
||
.eq("user_id", user.id)
|
||
.maybeSingle();
|
||
|
||
if (!staffUser?.is_staff) {
|
||
console.error("❌ [SEND SIGNATURE] Utilisateur non staff");
|
||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||
}
|
||
console.log("✅ [SEND SIGNATURE] Utilisateur est staff");
|
||
|
||
// Récupérer l'avenant avec les données du contrat, salarié et organisation
|
||
console.log("🔍 [SEND SIGNATURE] Récupération de l'avenant avec jointures...");
|
||
const { data: avenant, error: avenantError } = await supabase
|
||
.from("avenants")
|
||
.select(`
|
||
*,
|
||
cddu_contracts (
|
||
*,
|
||
salaries (
|
||
prenom,
|
||
nom,
|
||
adresse_mail
|
||
),
|
||
organizations (
|
||
id,
|
||
name,
|
||
organization_details (
|
||
email_signature,
|
||
prenom_signataire,
|
||
code_employeur
|
||
)
|
||
)
|
||
)
|
||
`)
|
||
.eq("id", id)
|
||
.single();
|
||
|
||
console.log("📊 [SEND SIGNATURE] Résultat requête avenant:", {
|
||
error: avenantError,
|
||
found: !!avenant,
|
||
hasContract: !!avenant?.cddu_contracts,
|
||
hasSalarie: !!avenant?.cddu_contracts?.salaries,
|
||
hasOrg: !!avenant?.cddu_contracts?.organizations,
|
||
hasOrgDetails: !!avenant?.cddu_contracts?.organizations?.organization_details
|
||
});
|
||
|
||
if (avenantError || !avenant) {
|
||
console.error("❌ [SEND SIGNATURE] Avenant non trouvé:", avenantError);
|
||
return NextResponse.json({ error: "Avenant non trouvé" }, { status: 404 });
|
||
}
|
||
|
||
const contract = avenant.cddu_contracts;
|
||
const salarie = contract.salaries;
|
||
const organization = contract.organizations;
|
||
const orgDetails = organization.organization_details;
|
||
|
||
console.log("📋 [SEND SIGNATURE] Données récupérées:", {
|
||
contractId: contract?.id,
|
||
salariePrenom: salarie?.prenom,
|
||
salarieNom: salarie?.nom,
|
||
salarieEmail: salarie?.adresse_mail,
|
||
orgName: organization?.name,
|
||
orgEmail: orgDetails?.email_signature,
|
||
pdfS3Key: avenant.pdf_s3_key
|
||
});
|
||
|
||
// Vérifier que le PDF existe
|
||
if (!avenant.pdf_s3_key) {
|
||
console.error("❌ [SEND SIGNATURE] PDF S3 key manquant");
|
||
return NextResponse.json(
|
||
{ error: "Le PDF de l'avenant doit être généré avant l'envoi en signature" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// Vérifier que les détails de l'organisation sont disponibles
|
||
if (!orgDetails || !orgDetails.email_signature) {
|
||
console.error("❌ [SEND SIGNATURE] Email signature manquant:", {
|
||
hasOrgDetails: !!orgDetails,
|
||
emailSignature: orgDetails?.email_signature
|
||
});
|
||
return NextResponse.json(
|
||
{ error: "Email de signature de l'organisation non configuré" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
console.log("✅ [SEND SIGNATURE] Validations passées, récupération du PDF depuis S3...");
|
||
|
||
// Récupérer le PDF depuis S3
|
||
const s3Client = new S3Client({
|
||
region: process.env.AWS_REGION || "eu-west-3",
|
||
credentials: {
|
||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||
},
|
||
});
|
||
|
||
const s3Object = await s3Client.send(
|
||
new GetObjectCommand({
|
||
Bucket: "odentas-docs",
|
||
Key: avenant.pdf_s3_key,
|
||
})
|
||
);
|
||
|
||
const pdfBuffer = await s3Object.Body?.transformToByteArray();
|
||
if (!pdfBuffer) {
|
||
console.error("❌ [SEND SIGNATURE] Impossible de lire le PDF depuis S3");
|
||
return NextResponse.json({ error: "Impossible de lire le PDF" }, { status: 500 });
|
||
}
|
||
|
||
console.log("✅ [SEND SIGNATURE] PDF récupéré depuis S3, taille:", pdfBuffer.length, "bytes");
|
||
|
||
const pdfBase64 = Buffer.from(pdfBuffer).toString("base64");
|
||
|
||
console.log("🔵 [SEND SIGNATURE] Création du template DocuSeal...");
|
||
|
||
// Créer le template DocuSeal
|
||
const templateResponse = await axios.post(
|
||
"https://api.docuseal.eu/templates/pdf",
|
||
{
|
||
name: `AVENANT - CDDU - ${contract.contract_number}`,
|
||
documents: [{ name: avenant.numero_avenant, file: pdfBase64 }],
|
||
},
|
||
{
|
||
headers: {
|
||
"X-Auth-Token": process.env.DOCUSEAL_TOKEN!,
|
||
"Content-Type": "application/json",
|
||
},
|
||
}
|
||
);
|
||
|
||
const templateId = templateResponse.data.id;
|
||
console.log("✅ [SEND SIGNATURE] Template DocuSeal créé:", templateId);
|
||
|
||
// Insérer les données dans DynamoDB pour la Lambda
|
||
console.log("🔵 [SEND SIGNATURE] Insertion des données dans DynamoDB...");
|
||
const dynamoDBClient = new DynamoDBClient({
|
||
region: process.env.AWS_REGION || "eu-west-3",
|
||
credentials: {
|
||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||
},
|
||
});
|
||
|
||
const formatDateForDisplay = (dateStr?: string) => {
|
||
if (!dateStr) return "-";
|
||
const [y, m, d] = dateStr.split("-");
|
||
return `${d}/${m}/${y}`;
|
||
};
|
||
|
||
try {
|
||
await dynamoDBClient.send(
|
||
new PutItemCommand({
|
||
TableName: "DocuSealNotification",
|
||
Item: {
|
||
submission_id: { S: avenant.numero_avenant }, // Clé primaire : numéro de l'avenant
|
||
employeeEmail: { S: salarie.adresse_mail },
|
||
reference: { S: contract.contract_number },
|
||
salarie: { S: `${salarie.prenom} ${salarie.nom}` },
|
||
date: { S: formatDateForDisplay(contract.date_debut) },
|
||
poste: { S: contract.fonction || "Non spécifié" },
|
||
analytique: { S: contract.analytique || "Non spécifié" },
|
||
structure: { S: organization.name },
|
||
prenom_salarie: { S: salarie.prenom },
|
||
prenom_signataire: { S: orgDetails.prenom_signataire || "Non spécifié" },
|
||
code_employeur: { S: orgDetails.code_employeur || "Non spécifié" },
|
||
matricule: { S: contract.matricule || "Non spécifié" },
|
||
created_at: { S: new Date().toISOString() },
|
||
},
|
||
})
|
||
);
|
||
console.log("✅ [SEND SIGNATURE] Données insérées dans DynamoDB");
|
||
} catch (dynamoError: any) {
|
||
console.error("❌ [SEND SIGNATURE] Erreur DynamoDB:", dynamoError);
|
||
// On continue quand même pour créer la soumission DocuSeal
|
||
}
|
||
|
||
console.log("🔵 [SEND SIGNATURE] Création de la soumission DocuSeal avec emails:", {
|
||
employeur: orgDetails.email_signature,
|
||
salarie: salarie.adresse_mail
|
||
});
|
||
|
||
// Créer la soumission DocuSeal
|
||
const submissionResponse = await axios.post(
|
||
"https://api.docuseal.eu/submissions",
|
||
{
|
||
template_id: templateId,
|
||
send_email: false,
|
||
submitters: [
|
||
{ role: "Employeur", email: orgDetails.email_signature },
|
||
{ role: "Salarié", email: salarie.adresse_mail },
|
||
],
|
||
},
|
||
{
|
||
headers: {
|
||
"X-Auth-Token": process.env.DOCUSEAL_TOKEN!,
|
||
"Content-Type": "application/json",
|
||
},
|
||
}
|
||
);
|
||
|
||
console.log("✅ [SEND SIGNATURE] Soumission DocuSeal créée:", submissionResponse.data);
|
||
|
||
const employerSubmission = submissionResponse.data.find((sub: any) => sub.role === "Employeur");
|
||
const employeeSubmission = submissionResponse.data.find((sub: any) => sub.role === "Salarié");
|
||
|
||
console.log("📋 [SEND SIGNATURE] Soumissions extraites:", {
|
||
hasEmployer: !!employerSubmission,
|
||
hasEmployee: !!employeeSubmission,
|
||
employerSlug: employerSubmission?.slug,
|
||
employeeSlug: employeeSubmission?.slug
|
||
});
|
||
|
||
if (!employerSubmission || !employeeSubmission) {
|
||
console.error("❌ [SEND SIGNATURE] Soumissions DocuSeal incomplètes");
|
||
return NextResponse.json(
|
||
{ error: "Impossible de créer les soumissions DocuSeal" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
const employerSlug = employerSubmission.slug;
|
||
const signatureLink = `https://staging.paie.odentas.fr/odentas-sign?docuseal_id=${employerSlug}`;
|
||
|
||
console.log("✅ [SEND SIGNATURE] Lien de signature généré:", signatureLink);
|
||
console.log("🔵 [SEND SIGNATURE] Mise à jour de l'avenant dans Supabase...");
|
||
|
||
// Mettre à jour l'avenant avec les informations DocuSeal
|
||
await supabase
|
||
.from("avenants")
|
||
.update({
|
||
docuseal_template_id: templateId,
|
||
docuseal_submission_id: employerSubmission.submission_id,
|
||
employer_docuseal_slug: employerSlug,
|
||
employee_docuseal_slug: employeeSubmission.slug,
|
||
signature_status: "pending_employer",
|
||
statut: "pending", // Passer de "draft" à "pending"
|
||
last_employer_notification_at: new Date().toISOString(),
|
||
})
|
||
.eq("id", id);
|
||
|
||
console.log("✅ [SEND SIGNATURE] Avenant mis à jour dans Supabase");
|
||
console.log("🔵 [SEND SIGNATURE] Envoi de l'email à l'employeur...");
|
||
|
||
// Formater la date de début
|
||
const formatDate = (dateStr?: string) => {
|
||
if (!dateStr) return "-";
|
||
const [y, m, d] = dateStr.split("-");
|
||
return `${d}/${m}/${y}`;
|
||
};
|
||
|
||
// Envoyer l'email via le système universel
|
||
try {
|
||
await sendUniversalEmailV2({
|
||
type: 'signature-request-employer',
|
||
toEmail: orgDetails.email_signature,
|
||
subject: `Signature électronique requise – Avenant ${avenant.numero_avenant}`,
|
||
data: {
|
||
firstName: orgDetails.prenom_signataire || undefined,
|
||
organizationName: organization.name,
|
||
employerCode: orgDetails.code_employeur || "Non spécifié",
|
||
handlerName: "Renaud BREVIERE-ABRAHAM",
|
||
documentType: `Avenant ${avenant.numero_avenant}`,
|
||
employeeName: `${salarie.prenom} ${salarie.nom}`,
|
||
contractReference: contract.contract_number,
|
||
status: 'En attente',
|
||
ctaUrl: signatureLink,
|
||
contractId: avenant.id, // Pour traçabilité dans email_logs
|
||
},
|
||
});
|
||
console.log("✅ [SEND SIGNATURE] Email envoyé avec succès");
|
||
} catch (emailError: any) {
|
||
console.error("❌ [SEND SIGNATURE] Erreur envoi email:", emailError);
|
||
// On continue quand même car DocuSeal est créé
|
||
}
|
||
|
||
console.log("🎉 [SEND SIGNATURE] Processus terminé avec succès");
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
message: "Avenant envoyé en signature à l'employeur",
|
||
signatureLink,
|
||
});
|
||
} catch (error: any) {
|
||
console.error("❌❌❌ [SEND SIGNATURE] ERREUR FATALE:", error);
|
||
console.error("Stack trace:", error.stack);
|
||
return NextResponse.json(
|
||
{ error: "Erreur serveur", details: error.message },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|