Correction problème relance signature salarié
This commit is contained in:
parent
6caf84c294
commit
2f147382b3
5 changed files with 273 additions and 8 deletions
72
SIGNATURE_SALARIE_FEATURE.md
Normal file
72
SIGNATURE_SALARIE_FEATURE.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Où `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>
|
||||||
|
```
|
||||||
|
|
||||||
|
Où `{{docuseal_slug}}` est le slug DocuSeal récupéré depuis l'API.
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
160
app/signature-salarie/SignatureSalarieContent.tsx
Normal file
160
app/signature-salarie/SignatureSalarieContent.tsx
Normal 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 été 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/signature-salarie/page.tsx
Normal file
21
app/signature-salarie/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 || '';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue