Verif DOB salarié signature

This commit is contained in:
odentas 2025-10-15 21:23:49 +02:00
parent 3eb696b45d
commit 8644e8860e
7 changed files with 500 additions and 37 deletions

View file

@ -0,0 +1,93 @@
import { NextRequest, NextResponse } from 'next/server';
import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService';
/**
* POST /api/emails/contact-support
*
* Route API pour envoyer un message de contact au support Odentas
* Utilise le système universel d'emails v2
*/
export async function POST(request: NextRequest) {
console.log('=== API Contact Support ===');
try {
// 1. Récupération et validation des données
const data = await request.json();
console.log('📦 Données reçues:', {
name: data.name,
email: data.email,
messageLength: data.message?.length || 0
});
const { name, email, message } = data;
// Validation des champs requis
if (!name || !email || !message) {
console.error('❌ Champs requis manquants');
return NextResponse.json(
{
error: 'Tous les champs sont requis',
success: false
},
{ status: 400 }
);
}
// Validation format email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
console.error('❌ Format email invalide');
return NextResponse.json(
{
error: 'Format d\'email invalide',
success: false
},
{ status: 400 }
);
}
// 2. Préparation des données de l'email
const emailData: EmailDataV2 = {
name: name,
email: email,
message: message,
submittedAt: new Date().toLocaleString('fr-FR', {
dateStyle: 'long',
timeStyle: 'short'
})
};
console.log('📧 Préparation de l\'envoi de l\'email au support');
// 3. Envoi de l'email via le système universel v2
const messageId = await sendUniversalEmailV2({
type: 'contact-support',
toEmail: 'paie@odentas.fr',
data: emailData,
});
console.log('✅ Email de contact envoyé avec succès:', {
messageId,
from: email,
name: name
});
// 4. Retour du succès
return NextResponse.json({
success: true,
messageId
});
} catch (error) {
console.error('❌ Erreur lors de l\'envoi de l\'email de contact:', error);
return NextResponse.json(
{
error: 'Échec de l\'envoi du message',
success: false,
message: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}

View file

@ -1,6 +1,7 @@
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
@ -108,7 +109,30 @@ export async function POST(request: NextRequest) {
reference
});
// 5. Retour du succès avec le messageId SES
// 5. Stocker le signature_link dans cddu_contracts pour permettre la vérification
if (contractId && signatureLink) {
console.log('💾 Stockage du signature_link dans cddu_contracts...');
try {
const supabase = createSbServiceRole();
const { error: updateError } = await supabase
.from('cddu_contracts')
.update({ signature_link: signatureLink })
.eq('id', contractId);
if (updateError) {
console.error('⚠️ Erreur lors de la mise à jour du signature_link:', updateError);
// Ne pas bloquer le flux, l'email est déjà envoyé
} else {
console.log('✅ signature_link stocké avec succès');
}
} catch (err) {
console.error('⚠️ Exception lors du stockage du signature_link:', err);
// Ne pas bloquer le flux
}
}
// 6. Retour du succès avec le messageId SES
return NextResponse.json({
success: true,
messageId,

View file

@ -26,29 +26,94 @@ export async function POST(request: NextRequest) {
);
}
console.log('🔍 Vérification pour docuseal_id:', docuseal_id);
console.log('🔍 Vérification pour docuseal_id (slug):', docuseal_id);
// Créer le client Supabase avec service role
const supabase = createSbServiceRole();
// 1. Récupérer l'email du salarié depuis la soumission DocuSeal
// On cherche dans cddu_contracts pour trouver le contrat lié au slug DocuSeal
// 1. Appeler l'API DocuSeal via le proxy interne pour récupérer les submissions et trouver celle avec ce slug
console.log('📞 Appel API DocuSeal (via proxy interne) pour trouver la submission avec le slug:', docuseal_id);
// Récupérer les submissions récentes via le proxy interne
let submissionId: string | null = null;
try {
// On récupère les 100 dernières submissions via le proxy interne
console.log('📞 Calling internal DocuSeal proxy: /api/docuseal/submissions?limit=100');
const docusealResponse = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions?limit=100`, {
method: 'GET',
cache: 'no-store',
});
console.log('📡 DocuSeal proxy response status:', docusealResponse.status);
if (!docusealResponse.ok) {
const errorText = await docusealResponse.text();
console.error('❌ Erreur DocuSeal proxy:', docusealResponse.status, errorText);
throw new Error(`DocuSeal proxy error: ${docusealResponse.status} - ${errorText}`);
}
const submissionsData = await docusealResponse.json();
console.log('📋 Type de données reçues:', typeof submissionsData, Array.isArray(submissionsData) ? 'array' : 'object');
// DocuSeal peut retourner soit un array, soit un objet avec data
const submissions = Array.isArray(submissionsData) ? submissionsData : (submissionsData.data || []);
console.log('📋 Nombre de submissions récupérées:', submissions.length);
// Chercher la submission qui contient ce slug dans un submitter
for (const submission of submissions) {
const submitters = submission.submitters || [];
const foundSubmitter = submitters.find((s: any) => s.slug === docuseal_id);
if (foundSubmitter) {
submissionId = submission.id;
console.log('✅ Submission trouvée:', submissionId, 'pour le slug:', docuseal_id);
break;
}
}
if (!submissionId) {
console.error('❌ Aucune submission trouvée avec le slug:', docuseal_id);
return NextResponse.json(
{ error: 'Document introuvable', verified: false },
{ status: 404 }
);
}
} catch (error) {
console.error('❌ Erreur lors de l\'appel DocuSeal:', error);
console.error('❌ Type d\'erreur:', error instanceof Error ? error.message : String(error));
console.error('❌ Stack:', error instanceof Error ? error.stack : 'N/A');
return NextResponse.json(
{
error: 'Erreur lors de la récupération du document',
verified: false,
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
// 2. Chercher le contrat dans cddu_contracts avec ce docuseal_submission_id
console.log('🔍 Recherche du contrat avec docuseal_submission_id:', submissionId);
const { data: contract, error: contractError } = await supabase
.from('cddu_contracts')
.select('employee_id, salarie_email')
.eq('docuseal_employee_slug', docuseal_id)
.select('id, employee_id, contract_number')
.eq('docuseal_submission_id', submissionId)
.maybeSingle();
if (contractError) {
console.error('❌ Erreur lors de la récupération du contrat:', contractError);
console.error('❌ Erreur lors de la recherche du contrat:', contractError);
return NextResponse.json(
{ error: 'Erreur lors de la vérification', verified: false },
{ error: 'Erreur lors de la recherche du contrat', verified: false },
{ status: 500 }
);
}
if (!contract) {
console.error('❌ Aucun contrat trouvé pour docuseal_id:', docuseal_id);
console.error('❌ Aucun contrat trouvé pour submission_id:', submissionId);
return NextResponse.json(
{ error: 'Document introuvable', verified: false },
{ status: 404 }
@ -56,14 +121,15 @@ export async function POST(request: NextRequest) {
}
console.log('📄 Contrat trouvé:', {
id: contract.id,
employee_id: contract.employee_id,
salarie_email: contract.salarie_email
contract_number: contract.contract_number
});
// 2. Récupérer la date de naissance du salarié depuis la table salaries
// 2. Récupérer les infos du salarié depuis la table salaries (y compris l'email)
const { data: salarie, error: salarieError } = await supabase
.from('salaries')
.select('date_naissance, prenom, nom')
.select('date_naissance, prenom, nom, adresse_mail')
.eq('id', contract.employee_id)
.maybeSingle();

View file

@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Calendar, Shield, AlertCircle, Loader2 } from "lucide-react";
import { Calendar, Shield, AlertCircle, Loader2, Lock } from "lucide-react";
interface BirthdateVerificationModalProps {
docuseal_id: string;
@ -54,7 +54,7 @@ export default function BirthdateVerificationModal({
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-md animate-in fade-in duration-200">
<div className="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden animate-in zoom-in-95 duration-300">
{/* Header avec gradient */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-5">
@ -114,17 +114,15 @@ export default function BirthdateVerificationModal({
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-900">{error}</p>
{attempts >= 2 && (
<p className="text-xs text-red-700 mt-1">
Besoin d'aide ? Contactez-nous à{' '}
<a
href="mailto:paie@odentas.fr"
className="underline hover:text-red-900"
>
paie@odentas.fr
</a>
</p>
)}
<p className="text-xs text-red-700 mt-1">
Besoin d'aide ? Contactez-nous à{' '}
<a
href="mailto:paie@odentas.fr"
className="underline hover:text-red-900"
>
paie@odentas.fr
</a>
</p>
</div>
</div>
</div>
@ -151,9 +149,12 @@ export default function BirthdateVerificationModal({
{/* Footer avec info */}
<div className="pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500 text-center leading-relaxed">
🔒 Vos données sont protégées et ne sont utilisées que pour vérifier votre identité.
Cette étape garantit que seul vous pouvez accéder à votre contrat.
<p className="text-xs text-gray-500 text-center leading-relaxed flex items-center justify-center gap-1.5">
<Lock className="w-3.5 h-3.5 flex-shrink-0" />
<span>
Vos données sont protégées et ne sont utilisées que pour vérifier votre identité.
Cette étape garantit que seul vous pouvez accéder à votre contrat.
</span>
</p>
</div>
</form>

View file

@ -0,0 +1,224 @@
"use client";
import { useState } from "react";
import { X, Send, Mail, User, MessageSquare, CheckCircle2, Loader2 } from "lucide-react";
interface ContactModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function ContactModal({ isOpen, onClose }: ContactModalProps) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setIsSending(true);
try {
const response = await fetch('/api/emails/contact-support', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
email,
message,
}),
});
const data = await response.json();
if (response.ok && data.success) {
setSuccess(true);
// Fermer après 2 secondes
setTimeout(() => {
onClose();
// Reset form
setName("");
setEmail("");
setMessage("");
setSuccess(false);
}, 2000);
} else {
setError(data.error || 'Erreur lors de l\'envoi du message');
}
} catch (err) {
console.error('Erreur lors de l\'envoi:', err);
setError('Erreur lors de l\'envoi du message. Veuillez réessayer.');
} finally {
setIsSending(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden animate-in zoom-in-95 duration-300">
{/* Header */}
<div className="relative bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-5">
<button
onClick={onClose}
className="absolute top-4 right-4 p-2 hover:bg-white/20 rounded-lg transition-colors"
type="button"
>
<X className="w-5 h-5 text-white" />
</button>
<div className="flex items-center gap-3">
<div className="p-2 bg-white/20 rounded-lg backdrop-blur-sm">
<Mail className="w-6 h-6 text-white" />
</div>
<div>
<h2 className="text-xl font-semibold text-white">
Contactez l'équipe Odentas
</h2>
<p className="text-blue-100 text-sm mt-0.5">
Nous sommes pour vous aider
</p>
</div>
</div>
</div>
{/* Corps */}
<div className="p-6">
{success ? (
<div className="py-8 text-center animate-in zoom-in duration-300">
<div className="inline-flex p-3 bg-green-100 rounded-full mb-4">
<CheckCircle2 className="w-12 h-12 text-green-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Message envoyé avec succès !
</h3>
<p className="text-sm text-gray-600">
Notre équipe vous répondra dans les plus brefs délais.
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Nom */}
<div className="space-y-2">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 flex items-center gap-2"
>
<User className="w-4 h-4 text-gray-500" />
Nom complet
</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={isSending}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all disabled:bg-gray-100 disabled:cursor-not-allowed"
placeholder="Jean Dupont"
/>
</div>
{/* Email */}
<div className="space-y-2">
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 flex items-center gap-2"
>
<Mail className="w-4 h-4 text-gray-500" />
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isSending}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all disabled:bg-gray-100 disabled:cursor-not-allowed"
placeholder="jean.dupont@exemple.fr"
/>
</div>
{/* Message */}
<div className="space-y-2">
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700 flex items-center gap-2"
>
<MessageSquare className="w-4 h-4 text-gray-500" />
Message
</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
required
disabled={isSending}
rows={5}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all disabled:bg-gray-100 disabled:cursor-not-allowed resize-none"
placeholder="Décrivez votre demande ou question..."
/>
</div>
{/* Message d'erreur */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-900">{error}</p>
</div>
)}
{/* Boutons */}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
disabled={isSending}
className="flex-1 px-4 py-2.5 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Annuler
</button>
<button
type="submit"
disabled={isSending}
className="flex-1 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-medium py-2.5 px-4 rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 shadow-lg hover:shadow-xl"
>
{isSending ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Envoi...
</>
) : (
<>
<Send className="w-4 h-4" />
Envoyer
</>
)}
</button>
</div>
{/* Info contact */}
<div className="pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500 text-center">
Vous pouvez également nous contacter directement à{' '}
<a
href="mailto:paie@odentas.fr"
className="text-blue-600 hover:text-blue-700 underline"
>
paie@odentas.fr
</a>
</p>
</div>
</form>
)}
</div>
</div>
</div>
);
}

View file

@ -2,10 +2,11 @@
import { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { AlertTriangle } from "lucide-react";
import { AlertTriangle, Mail } from "lucide-react";
import { usePageTitle } from "@/hooks/usePageTitle";
import Script from "next/script";
import BirthdateVerificationModal from "./BirthdateVerificationModal";
import ContactModal from "./ContactModal";
export default function SignatureSalarieContent() {
const searchParams = useSearchParams();
@ -13,6 +14,7 @@ export default function SignatureSalarieContent() {
const [showError, setShowError] = useState(false);
const [docusealLoaded, setDocusealLoaded] = useState(false);
const [isVerified, setIsVerified] = useState(false);
const [showContactModal, setShowContactModal] = useState(false);
// Définir le titre de la page
usePageTitle("Signature électronique");
@ -125,14 +127,30 @@ export default function SignatureSalarieContent() {
/>
<div className="min-h-screen bg-white">
{/* Header simple avec logo */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-center">
<img
src="https://ci3.googleusercontent.com/meips/ADKq_NaYk41Pm5XLYFH2kxxXdCOkuRa2Ji2vLeI41LRtc9oBmfS7-NhGtPUzYhw7arh9LdEyrUS--rk3mW1WSrKTtKjsTtiUcZVwfYFyqvV8YSZ8ZFJtQzqG43CgPmcZsSdWmIrxmPATULmFaFEpIIO-IdbHkat3RBeqwDQ=s0-d-e1-ft#https://newstaging.odentas.fr/wp-content/uploads/2025/08/Odentas-Logo-Bleu-Fond-Transparent-4-1.png"
alt="Odentas"
className="h-8"
/>
{/* Header avec logo agrandi et bouton contact */}
<div className="bg-white border-b border-gray-200 px-6 py-5">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex-1"></div>
<div className="flex flex-col items-center justify-center flex-1 gap-2">
<img
src="https://ci3.googleusercontent.com/meips/ADKq_NaYk41Pm5XLYFH2kxxXdCOkuRa2Ji2vLeI41LRtc9oBmfS7-NhGtPUzYhw7arh9LdEyrUS--rk3mW1WSrKTtKjsTtiUcZVwfYFyqvV8YSZ8ZFJtQzqG43CgPmcZsSdWmIrxmPATULmFaFEpIIO-IdbHkat3RBeqwDQ=s0-d-e1-ft#https://newstaging.odentas.fr/wp-content/uploads/2025/08/Odentas-Logo-Bleu-Fond-Transparent-4-1.png"
alt="Odentas"
className="h-12"
/>
<h1 className="text-lg font-semibold text-gray-800">
Signature Électronique
</h1>
</div>
<div className="flex-1 flex justify-end">
<button
onClick={() => setShowContactModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
>
<Mail className="w-4 h-4" />
<span className="hidden sm:inline">Contactez l'équipe Odentas</span>
<span className="sm:hidden">Contact</span>
</button>
</div>
</div>
</div>
@ -159,6 +177,12 @@ export default function SignatureSalarieContent() {
</div>
</div>
{/* Modal de contact */}
<ContactModal
isOpen={showContactModal}
onClose={() => setShowContactModal(false)}
/>
<style jsx global>{`
/* Styles spécifiques pour Safari iOS si nécessaire */
.safari_only {

View file

@ -43,6 +43,7 @@ export type EmailTypeV2 =
| 'support-reply' // Réponse du staff à un ticket support
| 'support-ticket-created' // Notification interne : nouveau ticket créé
| 'support-ticket-reply' // Notification interne : réponse utilisateur à un ticket
| 'contact-support' // Formulaire de contact public vers le support
// Accès / habilitations
| 'account-activation'
| 'access-updated'
@ -964,6 +965,36 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
{ label: 'Réponse', key: 'userMessage' },
]
}
},
'contact-support': {
subject: '[CONTACT] Nouveau message de {{name}}',
title: '📧 Nouveau message de contact',
greeting: 'Équipe Support',
mainMessage: 'Vous avez reçu un nouveau message via le formulaire de contact.',
ctaText: 'Répondre par email',
footerText: 'Message envoyé depuis le formulaire de contact Odentas.',
preheaderText: 'Nouveau message de contact',
colors: {
headerColor: STANDARD_COLORS.HEADER,
titleColor: '#0F172A',
buttonColor: STANDARD_COLORS.BUTTON,
buttonTextColor: STANDARD_COLORS.BUTTON_TEXT,
cardBackgroundColor: '#FFFFFF',
cardBorder: '#E5E7EB',
cardTitleColor: '#0F172A',
},
infoCard: [
{ label: 'Nom', key: 'name' },
{ label: 'Email', key: 'email' },
{ label: 'Envoyé le', key: 'submittedAt' },
],
detailsCard: {
title: 'Message',
rows: [
{ label: 'Contenu', key: 'message' },
]
}
}
};