espace-paie-odentas/app/api/staff/contrats/[id]/remind-employer/route.ts
odentas d7bdb1ef08 feat: Add notification tracking system with smart reminders
- 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
2025-10-22 21:49:35 +02:00

294 lines
No EOL
10 KiB
TypeScript
Raw Blame History

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()
});
}