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 { cookies } from 'next/headers';
|
||||
import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService';
|
||||
import { ENV } from '@/lib/cleanEnv';
|
||||
// Envoi via le système universel (pas de configuration SES directe ici)
|
||||
|
||||
// POST /api/signatures-electroniques/relance
|
||||
|
|
@ -85,16 +86,26 @@ export async function POST(req: NextRequest) {
|
|||
let docusealEmail: string | null = null;
|
||||
if (contract.docuseal_submission_id) {
|
||||
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) {
|
||||
const docusealData = await docusealResponse.json();
|
||||
const submitters = docusealData?.submitters || [];
|
||||
const employeeSubmitter = submitters.find((s: any) => s.role === 'Salarié');
|
||||
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) {
|
||||
docusealEmail = String(employeeSubmitter.email);
|
||||
}
|
||||
} else {
|
||||
console.error('DocuSeal API error:', docusealResponse.status, await docusealResponse.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération DocuSeal:', error);
|
||||
|
|
@ -102,10 +113,11 @@ export async function POST(req: NextRequest) {
|
|||
}
|
||||
|
||||
// Construction du lien de signature
|
||||
let signatureLink = contract.signature_link as string | null;
|
||||
if (!signatureLink && employeeSlug) {
|
||||
const siteBase = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_ENV === 'production' ? 'https://paie.odentas.fr' : 'https://staging.paie.odentas.fr');
|
||||
signatureLink = `${siteBase}/odentas-sign?docuseal_id=${employeeSlug}`;
|
||||
// TOUJOURS utiliser le slug du salarié (pas celui de l'employeur stocké dans signature_link)
|
||||
let signatureLink: string | null = null;
|
||||
if (employeeSlug) {
|
||||
const siteBase = process.env.NEXT_PUBLIC_SITE_URL || 'https://paie.odentas.fr';
|
||||
signatureLink = `${siteBase}/signature-salarie?docuseal_id=${employeeSlug}`;
|
||||
}
|
||||
|
||||
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
|
||||
// É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') ||
|
||||
path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile;
|
||||
const isMaintenancePage = path === '/maintenance';
|
||||
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
|
||||
const hostname = req.nextUrl.hostname || '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue