Correction problème relance signature salarié

This commit is contained in:
odentas 2025-10-13 01:06:20 +02:00
parent 6caf84c294
commit 2f147382b3
5 changed files with 273 additions and 8 deletions

View file

@ -0,0 +1,72 @@
# Page de Signature pour les Salariés
## 📄 Fichiers créés
### 1. `/app/signature-salarie/page.tsx`
Page principale qui charge le composant de signature dans un Suspense
### 2. `/app/signature-salarie/SignatureSalarieContent.tsx`
Composant client qui affiche le formulaire DocuSeal
### 3. Modification du `/middleware.ts`
Ajout de `/signature-salarie` aux pages publiques (pas d'authentification requise)
## 🎯 Fonctionnalités
**Page publique** - Accessible sans authentification
**Sans Header/Sidebar** - Design épuré comme `dl-contrat-signe`
**DocuSeal intégré** - Widget de signature électronique
**Détection d'erreurs** - Gestion des documents inexistants
**Safari iOS compatible** - Classe CSS spéciale si nécessaire
**Événement de complétion** - Log quand le formulaire est signé
## 🔗 Utilisation
La page est accessible via :
```
https://paie.odentas.fr/signature-salarie?docuseal_id=XXXXX
```
`XXXXX` est le slug DocuSeal du document à signer.
## 📱 Options DocuSeal configurées
- ✅ Langue française (`data-language="fr"`)
- ✅ Pas de bouton d'envoi de copie (`data-with-send-copy-button="false"`)
- ✅ Pas de ressoumission (`data-allow-to-resubmit="false"`)
- ✅ Pas de signature tapée (`data-allow-typed-signature="false"`)
- ✅ Mémorisation de la signature (`data-remember-signature="true"`)
- ✅ Pas de titre (`data-with-title="false"`)
- ✅ Message de complétion personnalisé
## 🔧 Messages d'erreur
### Lien invalide
Si aucun `docuseal_id` n'est fourni dans l'URL, affiche :
> "Le lien de signature est invalide ou incomplet. Veuillez vérifier le lien reçu par email."
### Document inexistant
Si DocuSeal retourne une erreur "Unable to find form with slug", affiche :
> "Ce document n'existe pas ou a été supprimé pour cause de modification. Dans ce cas, vous avez reçu un e-mail pour signer la nouvelle version."
## 🎨 Design
- Header simple avec logo Odentas centré
- Fond blanc épuré
- Conteneur max-width 4xl pour le formulaire
- Responsive et mobile-friendly
## 🚀 Déploiement
Aucune configuration supplémentaire nécessaire. Les fichiers sont prêts à être déployés.
## 📧 Intégration avec les emails
Pour utiliser cette page dans les emails de signature envoyés aux salariés, utilisez le lien :
```html
<a href="https://paie.odentas.fr/signature-salarie?docuseal_id={{docuseal_slug}}">
Signer le document
</a>
```
`{{docuseal_slug}}` est le slug DocuSeal récupéré depuis l'API.

View file

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService'; import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService';
import { ENV } from '@/lib/cleanEnv';
// Envoi via le système universel (pas de configuration SES directe ici) // Envoi via le système universel (pas de configuration SES directe ici)
// POST /api/signatures-electroniques/relance // POST /api/signatures-electroniques/relance
@ -85,16 +86,26 @@ export async function POST(req: NextRequest) {
let docusealEmail: string | null = null; let docusealEmail: string | null = null;
if (contract.docuseal_submission_id) { if (contract.docuseal_submission_id) {
try { try {
const docusealResponse = await fetch(`${process.env.NEXT_PUBLIC_API_BASE || ''}/api/docuseal/submissions/${contract.docuseal_submission_id}`); // Appel direct à l'API DocuSeal (pas de route interne)
const docusealResponse = await fetch(`https://api.docuseal.eu/submissions/${contract.docuseal_submission_id}`, {
method: 'GET',
headers: {
'X-Auth-Token': ENV.DOCUSEAL_TOKEN,
'Content-Type': 'application/json',
},
});
if (docusealResponse.ok) { if (docusealResponse.ok) {
const docusealData = await docusealResponse.json(); const docusealData = await docusealResponse.json();
const submitters = docusealData?.submitters || []; const submitters = docusealData?.submitters || [];
const employeeSubmitter = submitters.find((s: any) => s.role === 'Salarié'); const employeeSubmitter = submitters.find((s: any) => s.role === 'Salarié');
employeeSlug = employeeSubmitter?.slug || null; employeeSlug = employeeSubmitter?.slug || null;
// Fallback email via DocuSeal si introuvable en base salaires (voir plus bas) // Fallback email via DocuSeal si introuvable en base salaries (voir plus bas)
if (employeeSubmitter?.email) { if (employeeSubmitter?.email) {
docusealEmail = String(employeeSubmitter.email); docusealEmail = String(employeeSubmitter.email);
} }
} else {
console.error('DocuSeal API error:', docusealResponse.status, await docusealResponse.text());
} }
} catch (error) { } catch (error) {
console.error('Erreur récupération DocuSeal:', error); console.error('Erreur récupération DocuSeal:', error);
@ -102,10 +113,11 @@ export async function POST(req: NextRequest) {
} }
// Construction du lien de signature // Construction du lien de signature
let signatureLink = contract.signature_link as string | null; // TOUJOURS utiliser le slug du salarié (pas celui de l'employeur stocké dans signature_link)
if (!signatureLink && employeeSlug) { let signatureLink: string | null = null;
const siteBase = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_ENV === 'production' ? 'https://paie.odentas.fr' : 'https://staging.paie.odentas.fr'); if (employeeSlug) {
signatureLink = `${siteBase}/odentas-sign?docuseal_id=${employeeSlug}`; const siteBase = process.env.NEXT_PUBLIC_SITE_URL || 'https://paie.odentas.fr';
signatureLink = `${siteBase}/signature-salarie?docuseal_id=${employeeSlug}`;
} }
if (!signatureLink) { if (!signatureLink) {

View file

@ -0,0 +1,160 @@
"use client";
import { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { AlertTriangle } from "lucide-react";
import { usePageTitle } from "@/hooks/usePageTitle";
import Script from "next/script";
export default function SignatureSalarieContent() {
const searchParams = useSearchParams();
const docusealId = searchParams.get("docuseal_id");
const [showError, setShowError] = useState(false);
const [docusealLoaded, setDocusealLoaded] = useState(false);
// Définir le titre de la page
usePageTitle("Signature électronique");
// Détecter Safari iOS et ajouter une classe
useEffect(() => {
const isSafariIOS = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) && /iPhone|iPad|iPod/.test(navigator.userAgent);
if (isSafariIOS) {
document.documentElement.classList.add('safari_only');
}
}, []);
// Intercepter les erreurs console pour détecter les problèmes DocuSeal
useEffect(() => {
const originalConsoleError = console.error;
console.error = function(...args: any[]) {
if (
args.length > 0 &&
typeof args[0] === 'string' &&
args[0].includes("Unable to find form with slug")
) {
setShowError(true);
}
originalConsoleError.apply(console, args);
};
return () => {
console.error = originalConsoleError;
};
}, []);
// Écouter l'événement de complétion
useEffect(() => {
if (!docusealLoaded) return;
const form = document.getElementById('docusealForm');
if (!form) return;
const handleCompleted = (e: any) => {
console.log('Formulaire DocuSeal complété:', e.detail);
};
form.addEventListener('completed', handleCompleted);
return () => {
form.removeEventListener('completed', handleCompleted);
};
}, [docusealLoaded]);
if (!docusealId) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="max-w-md mx-auto text-center p-6 bg-white rounded-lg shadow-lg">
<div className="text-red-600 mb-4">
<AlertTriangle className="w-12 h-12 mx-auto" />
</div>
<h1 className="text-xl font-semibold text-gray-900 mb-2">Lien invalide</h1>
<p className="text-gray-600 mb-4">
Le lien de signature est invalide ou incomplet. Veuillez vérifier le lien reçu par email.
</p>
<p className="text-sm text-gray-500">
Besoin d'aide ? Contactez-nous à{' '}
<a href="mailto:paie@odentas.fr" className="text-blue-600 hover:underline">
paie@odentas.fr
</a>
</p>
</div>
</div>
);
}
if (showError) {
return (
<div className="flex items-center justify-center min-h-screen p-4">
<div className="max-w-md mx-auto text-center p-6 bg-white rounded-lg shadow-lg">
<div className="text-amber-600 mb-4">
<AlertTriangle className="w-12 h-12 mx-auto" />
</div>
<h1 className="text-xl font-semibold text-gray-900 mb-2">Document inexistant</h1>
<p className="text-gray-600 mb-4">
Ce document n'existe pas ou a é supprimé pour cause de modification. Dans ce cas, vous avez reçu un e-mail pour signer la nouvelle version.
</p>
<p className="text-sm text-gray-500">
Contactez-nous à{' '}
<a href="mailto:paie@odentas.fr" className="text-blue-600 hover:underline">
paie@odentas.fr
</a>
{' '}si vous avez besoin d'assistance.
</p>
</div>
</div>
);
}
return (
<>
{/* Charger le script DocuSeal */}
<Script
src="https://cdn.docuseal.com/js/form.js"
onLoad={() => setDocusealLoaded(true)}
strategy="afterInteractive"
/>
<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"
/>
</div>
</div>
{/* Conteneur du formulaire DocuSeal */}
<div id="docusealWrapper" className="max-w-4xl mx-auto p-4">
<div
id="docusealForm"
dangerouslySetInnerHTML={{
__html: `
<docuseal-form
data-src="https://docuseal.eu/s/${docusealId}"
data-language="fr"
data-with-send-copy-button="false"
data-allow-to-resubmit="false"
data-allow-typed-signature="false"
data-remember-signature="true"
data-with-title="false"
data-completed-message-body="Le document a été signé."
>
</docuseal-form>
`
}}
/>
</div>
</div>
<style jsx global>{`
/* Styles spécifiques pour Safari iOS si nécessaire */
.safari_only {
/* Ajoutez des styles spécifiques pour Safari iOS ici si nécessaire */
}
`}</style>
</>
);
}

View file

@ -0,0 +1,21 @@
"use client";
import { Suspense } from "react";
import SignatureSalarieContent from "./SignatureSalarieContent";
export default function SignatureSalariePage() {
return (
<div className="min-h-screen bg-gray-50">
<Suspense fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<div className="text-gray-600">Chargement du formulaire de signature...</div>
</div>
</div>
}>
<SignatureSalarieContent />
</Suspense>
</div>
);
}

View file

@ -49,12 +49,12 @@ export async function middleware(req: NextRequest) {
// 2) Vérification du mode maintenance // 2) Vérification du mode maintenance
// Éviter de vérifier sur les API calls, assets, la page maintenance elle-même, la page de connexion, // Éviter de vérifier sur les API calls, assets, la page maintenance elle-même, la page de connexion,
// et les pages publiques (auto-declaration, dl-contrat-signe) // et les pages publiques (auto-declaration, dl-contrat-signe, signature-salarie)
const isApiOrAssets = path.startsWith('/api') || path.startsWith('/_next') || const isApiOrAssets = path.startsWith('/api') || path.startsWith('/_next') ||
path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile; path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile;
const isMaintenancePage = path === '/maintenance'; const isMaintenancePage = path === '/maintenance';
const isSigninPage = path === '/signin'; const isSigninPage = path === '/signin';
const isPublicPage = path.startsWith('/auto-declaration') || path.startsWith('/dl-contrat-signe'); const isPublicPage = path.startsWith('/auto-declaration') || path.startsWith('/dl-contrat-signe') || path.startsWith('/signature-salarie');
// Ne pas impacter l'environnement local/dev par le mode maintenance // Ne pas impacter l'environnement local/dev par le mode maintenance
const hostname = req.nextUrl.hostname || ''; const hostname = req.nextUrl.hostname || '';