- 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
294 lines
No EOL
10 KiB
TypeScript
294 lines
No EOL
10 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import { createSbServer } from "@/lib/supabaseServer";
|
||
import { sendUniversalEmailV2, EmailDataV2 } from "@/lib/emailTemplateService";
|
||
|
||
// Envoi d'email via le système universel (SES est géré en interne)
|
||
|
||
// Configuration DocuSeal
|
||
const DOCUSEAL_API_URL = 'api.docuseal.eu';
|
||
const DOCUSEAL_API_KEY = process.env.DOCUSEAL_API_KEY || process.env.DOCUSEAL_TOKEN;
|
||
|
||
// Fonction pour formater une date ISO au format DD/MM/AAAA
|
||
const formatDate = (isoDate: string) => {
|
||
const date = new Date(isoDate);
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const year = date.getFullYear();
|
||
return `${day}/${month}/${year}`;
|
||
};
|
||
|
||
// Fonction pour récupérer le slug employeur depuis DocuSeal
|
||
async function getEmployerSlug(docusealSubID: string): Promise<string> {
|
||
console.log("🔗 [REMIND-EMPLOYER] Récupération slug DocuSeal:", docusealSubID);
|
||
|
||
// Mode simulation si pas de clé API DocuSeal
|
||
if (!DOCUSEAL_API_KEY) {
|
||
console.log("⚠️ [REMIND-EMPLOYER] Mode simulation - Pas de clé API DocuSeal");
|
||
return `test-slug-${docusealSubID}`;
|
||
}
|
||
|
||
const response = await fetch(`https://${DOCUSEAL_API_URL}/submissions/${docusealSubID}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-Auth-Token': DOCUSEAL_API_KEY || '',
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.log("❌ [REMIND-EMPLOYER] Erreur DocuSeal API:", {
|
||
status: response.status,
|
||
statusText: response.statusText
|
||
});
|
||
throw new Error(`Erreur DocuSeal API: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log("📋 [REMIND-EMPLOYER] Réponse DocuSeal:", { submitters: data.submitters?.length || 0 });
|
||
|
||
const employerSubmitter = data.submitters?.find((submitter: any) => submitter.role === 'Employeur');
|
||
|
||
if (!employerSubmitter || !employerSubmitter.slug) {
|
||
throw new Error('Slug du rôle Employeur non trouvé.');
|
||
}
|
||
|
||
return employerSubmitter.slug;
|
||
}
|
||
|
||
// Suppression du HTML personnalisé et de l'envoi direct via SES.
|
||
// L'envoi s'effectue désormais via le système d'email universel.
|
||
|
||
export async function POST(
|
||
request: NextRequest,
|
||
{ params }: { params: { id: string } }
|
||
) {
|
||
console.log("🚀 [REMIND-EMPLOYER] Début de la requête POST");
|
||
console.log("📋 [REMIND-EMPLOYER] Paramètres reçus:", {
|
||
contractId: params.id,
|
||
method: request.method,
|
||
url: request.url
|
||
});
|
||
|
||
const sb = createSbServer();
|
||
|
||
try {
|
||
console.log("🔐 [REMIND-EMPLOYER] Vérification de l'authentification...");
|
||
|
||
// Vérification de l'authentification
|
||
const { data: { user }, error: authError } = await sb.auth.getUser();
|
||
if (authError || !user) {
|
||
console.log("❌ [REMIND-EMPLOYER] Échec de l'authentification:", { authError, hasUser: !!user });
|
||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||
}
|
||
|
||
console.log("✅ [REMIND-EMPLOYER] Utilisateur authentifié:", {
|
||
userId: user.id,
|
||
email: user.email
|
||
});
|
||
|
||
console.log("👤 [REMIND-EMPLOYER] Vérification des droits staff...");
|
||
|
||
// Vérification des droits staff
|
||
const { data: staffUser } = await sb
|
||
.from("staff_users")
|
||
.select("is_staff")
|
||
.eq("user_id", user.id)
|
||
.maybeSingle();
|
||
|
||
console.log("🔍 [REMIND-EMPLOYER] Résultat vérification staff:", {
|
||
staffUser,
|
||
isStaff: staffUser?.is_staff
|
||
});
|
||
|
||
if (!staffUser?.is_staff) {
|
||
console.log("❌ [REMIND-EMPLOYER] Accès refusé - utilisateur non staff");
|
||
return NextResponse.json({ error: "Accès refusé - réservé au staff" }, { status: 403 });
|
||
}
|
||
|
||
console.log("📄 [REMIND-EMPLOYER] Récupération du contrat:", params.id);
|
||
|
||
// Récupération du contrat avec toutes les données nécessaires
|
||
const { data: contract, error: contractError } = await sb
|
||
.from("cddu_contracts")
|
||
.select(`
|
||
id, reference, contract_number, employee_name, org_id,
|
||
docuseal_submission_id, analytique, production_name,
|
||
start_date, profession, role, employee_matricule
|
||
`)
|
||
.eq("id", params.id)
|
||
.single();
|
||
|
||
if (contractError || !contract) {
|
||
console.log("❌ [REMIND-EMPLOYER] Contrat non trouvé:", {
|
||
contractError,
|
||
contractId: params.id,
|
||
hasContract: !!contract
|
||
});
|
||
return NextResponse.json({ error: "Contrat non trouvé" }, { status: 404 });
|
||
}
|
||
|
||
console.log("✅ [REMIND-EMPLOYER] Contrat trouvé:", {
|
||
contractId: contract.id,
|
||
reference: contract.reference || contract.contract_number,
|
||
employee: contract.employee_name,
|
||
docusealId: contract.docuseal_submission_id,
|
||
orgId: contract.org_id
|
||
});
|
||
|
||
// Vérification que le contrat a un DocuSeal submission ID
|
||
if (!contract.docuseal_submission_id) {
|
||
console.log("❌ [REMIND-EMPLOYER] Pas d'ID DocuSeal:", {
|
||
contractId: contract.id,
|
||
docusealSubmissionId: contract.docuseal_submission_id
|
||
});
|
||
return NextResponse.json({
|
||
error: "Contrat sans ID DocuSeal - impossible d'envoyer une relance"
|
||
}, { status: 400 });
|
||
}
|
||
|
||
console.log("🏢 [REMIND-EMPLOYER] Récupération organisation:", contract.org_id);
|
||
|
||
// Récupération des détails de l'organisation depuis organization_details
|
||
const { data: orgDetails, error: orgError } = await sb
|
||
.from("organization_details")
|
||
.select("email_signature, code_employeur, prenom_signataire")
|
||
.eq("org_id", contract.org_id)
|
||
.single();
|
||
|
||
if (orgError || !orgDetails) {
|
||
console.log("❌ [REMIND-EMPLOYER] Organization_details non trouvée:", {
|
||
orgError,
|
||
orgId: contract.org_id
|
||
});
|
||
return NextResponse.json({
|
||
error: "Détails de l'organisation non trouvés"
|
||
}, { status: 404 });
|
||
}
|
||
|
||
// Récupération du nom de l'organisation depuis organizations
|
||
const { data: org, error: orgNameError } = await sb
|
||
.from("organizations")
|
||
.select("name")
|
||
.eq("id", contract.org_id)
|
||
.single();
|
||
|
||
if (orgNameError || !org) {
|
||
console.log("❌ [REMIND-EMPLOYER] Organisation name non trouvée:", {
|
||
orgNameError,
|
||
orgId: contract.org_id
|
||
});
|
||
return NextResponse.json({
|
||
error: "Nom de l'organisation non trouvé"
|
||
}, { status: 404 });
|
||
}
|
||
|
||
console.log("✅ [REMIND-EMPLOYER] Organisation trouvée:", {
|
||
orgName: org.name,
|
||
email: orgDetails.email_signature ? 'présent' : 'absent',
|
||
code: orgDetails.code_employeur
|
||
});
|
||
|
||
// Construction des données du contrat
|
||
const contractData = {
|
||
analytique: contract.analytique || contract.production_name || '',
|
||
date: contract.start_date,
|
||
employerEmail: orgDetails.email_signature,
|
||
matricule: contract.employee_matricule || '',
|
||
poste: contract.profession || contract.role || '',
|
||
prenom_signataire: orgDetails.prenom_signataire || '',
|
||
reference: contract.reference || contract.contract_number || '',
|
||
structure: org.name || '',
|
||
docusealSubID: contract.docuseal_submission_id,
|
||
typecontrat: 'CDDU',
|
||
code_employeur: orgDetails.code_employeur || '',
|
||
salarie: contract.employee_name || ''
|
||
};
|
||
|
||
// Vérification des données essentielles
|
||
if (!contractData.employerEmail) {
|
||
console.log("❌ [REMIND-EMPLOYER] Email employeur manquant");
|
||
return NextResponse.json({
|
||
error: "Email employeur manquant dans organization_details"
|
||
}, { status: 400 });
|
||
}
|
||
|
||
console.log("🔗 [REMIND-EMPLOYER] Récupération lien signature DocuSeal...");
|
||
|
||
// Récupération du slug employeur depuis DocuSeal (pour référence)
|
||
const employerSlug = await getEmployerSlug(contractData.docusealSubID);
|
||
const signatureLink = `https://staging.paie.odentas.fr/odentas-sign?docuseal_id=${employerSlug}`;
|
||
|
||
console.log("📧 [REMIND-EMPLOYER] Envoi email de rappel via template universel...");
|
||
|
||
// Préparation des données au format universel
|
||
// Le bouton redirige maintenant vers la page des signatures électroniques
|
||
const emailData: EmailDataV2 = {
|
||
firstName: contractData.prenom_signataire || 'Employeur',
|
||
organizationName: contractData.structure,
|
||
employerCode: contractData.code_employeur,
|
||
employeeName: contractData.salarie,
|
||
documentType: contractData.typecontrat || 'Contrat de travail',
|
||
contractReference: contractData.reference,
|
||
status: 'En attente',
|
||
ctaUrl: 'https://paie.odentas.fr/signatures-electroniques',
|
||
contractId: params.id, // Ajout pour traçabilité dans email_logs
|
||
};
|
||
|
||
await sendUniversalEmailV2({
|
||
type: 'signature-request-employer',
|
||
toEmail: contractData.employerEmail,
|
||
subject: `[Rappel] Signature électronique - ${contractData.reference}`,
|
||
data: emailData,
|
||
});
|
||
|
||
console.log("✅ [REMIND-EMPLOYER] Email envoyé avec succès via système universel");
|
||
|
||
// Mettre à jour le timestamp de dernière notification employeur
|
||
const now = new Date().toISOString();
|
||
const { error: updateError } = await sb
|
||
.from('cddu_contracts')
|
||
.update({ last_employer_notification_at: now })
|
||
.eq('id', params.id);
|
||
|
||
if (updateError) {
|
||
console.warn("⚠️ [REMIND-EMPLOYER] Impossible de mettre à jour last_employer_notification_at:", updateError);
|
||
} else {
|
||
console.log("✅ [REMIND-EMPLOYER] Timestamp de notification enregistré:", now);
|
||
}
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
message: "Relance employeur envoyée avec succès",
|
||
data: {
|
||
contractId: params.id,
|
||
contractRef: contractData.reference,
|
||
employerEmail: contractData.employerEmail,
|
||
emailSent: true,
|
||
signatureLink,
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error("💥 [REMIND-EMPLOYER] Erreur lors du test:", error);
|
||
console.error("📋 [REMIND-EMPLOYER] Stack trace:", error instanceof Error ? error.stack : 'No stack');
|
||
return NextResponse.json({
|
||
error: "Erreur lors du test de l'endpoint",
|
||
details: error instanceof Error ? error.message : String(error)
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
export async function GET(
|
||
request: NextRequest,
|
||
{ params }: { params: { id: string } }
|
||
) {
|
||
console.log("<22> [REMIND-EMPLOYER] GET Request reçue - endpoint actif");
|
||
console.log("📋 [REMIND-EMPLOYER] Paramètres GET:", { contractId: params.id });
|
||
|
||
return NextResponse.json({
|
||
message: "Endpoint remind-employer actif",
|
||
contractId: params.id,
|
||
method: "GET",
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
} |