diff --git a/DOCUSEAL_DEBUG_GUIDE.md b/DOCUSEAL_DEBUG_GUIDE.md new file mode 100644 index 0000000..f1a8bd3 --- /dev/null +++ b/DOCUSEAL_DEBUG_GUIDE.md @@ -0,0 +1,299 @@ +# Guide de Débogage - Pré-remplissage Signature DocuSeal + +## Date: 14 octobre 2025 + +## Logs à vérifier dans la console + +Lorsque vous ouvrez une signature électronique, vous devriez voir cette séquence de logs : + +### 1. API Backend (`/api/contrats/[id]/signature`) + +``` +🔍 [API SIGNATURE] Recherche contrat ID: xxx +✅ [API SIGNATURE] Contrat trouvé: { id, org_id, ... } +🔍 [API SIGNATURE] org_id présent: xxx +🔍 [API SIGNATURE] Recherche de signature pour org_id... +📋 [API SIGNATURE] Résultat requête organization_details: { + hasData: true/false, + hasError: true/false, + hasSignature: true/false, + signatureLength: xxx, + signaturePreview: "data:image/png;base64,..." +} +✅ [API SIGNATURE] Signature trouvée pour l'organisation +📏 [API SIGNATURE] Longueur de la signature: xxx +📤 [API SIGNATURE] Réponse finale: { + hasSignature: true, + signatureLength: xxx, + contractId: xxx, + orgId: xxx +} +``` + +### 2. Frontend - Récupération des données + +``` +🔍 [SIGNATURE] Debug - data complète: { ... } +📋 [SIGNATURE] Données contrat depuis API: { success: true, data: {...} } +🔍 [SIGNATURE] result.data: { ... } +🔍 [SIGNATURE] result.data.signature_b64: "data:image/png;base64,..." +📦 [SIGNATURE] contractData extrait: { ... } +🖊️ [SIGNATURE] signatureB64 extraite: { + exists: true, + type: "string", + length: xxx, + preview: "data:image/png;base64,..." +} +✅ [SIGNATURE] Signature B64 disponible, longueur: xxx +``` + +### 3. Frontend - Stockage sessionStorage + +``` +🔍 [SIGNATURE] Tentative de stockage de la signature... +🔍 [SIGNATURE] signatureB64 value: "data:image/png;base64,..." +📝 [SIGNATURE] Stockage de la signature dans sessionStorage +📝 [SIGNATURE] Longueur de la signature: xxx +📝 [SIGNATURE] Preview: "data:image/png;base64,..." +✅ [SIGNATURE] Signature stockée avec succès, vérification: { + stored: true, + length: xxx, + matches: true +} +✅ [SIGNATURE] URL embed trouvée: https://docuseal.eu/s/xxx +``` + +### 4. Frontend - Rendu du composant + +``` +🎨 [SIGNATURE RENDER] Génération du HTML docuseal-form +🎨 [SIGNATURE RENDER] embedSrc: https://docuseal.eu/s/xxx +🎨 [SIGNATURE RENDER] Signature depuis sessionStorage: { + exists: true, + length: xxx, + preview: "data:image/png;base64,..." +} +🎨 [SIGNATURE RENDER] signatureAttr généré: { + hasAttr: true, + attrLength: xxx, + attrPreview: 'data-signature="data:image/png;base64,...' +} +🎨 [SIGNATURE RENDER] HTML final généré +🎨 [SIGNATURE RENDER] HTML length: xxx +🎨 [SIGNATURE RENDER] HTML contient data-signature: true +``` + +## Points de vérification + +### Étape 1 : Vérifier la base de données + +```sql +-- Vérifier que la signature existe +SELECT + org_id, + structure, + LENGTH(signature_b64) as signature_length, + SUBSTRING(signature_b64, 1, 50) as signature_preview +FROM organization_details +WHERE signature_b64 IS NOT NULL; +``` + +**Attendu** : +- Au moins une ligne avec signature_b64 non NULL +- signature_length > 0 +- signature_preview commence par "data:image/" + +### Étape 2 : Vérifier que le contrat a un org_id + +```sql +-- Vérifier le contrat +SELECT + id, + contract_number, + org_id, + signature_status +FROM cddu_contracts +WHERE id = 'VOTRE_CONTRACT_ID'; +``` + +**Attendu** : +- org_id non NULL +- org_id correspond à une organisation avec signature_b64 + +### Étape 3 : Tester l'API directement + +```bash +# Remplacer CONTRACT_ID par l'ID réel +curl http://localhost:3000/api/contrats/CONTRACT_ID/signature +``` + +**Attendu dans la réponse** : +```json +{ + "success": true, + "data": { + "id": "...", + "org_id": "...", + "signature_b64": "data:image/png;base64,..." + } +} +``` + +### Étape 4 : Vérifier la console du navigateur + +**Ouvrir DevTools (F12) → Console** + +Chercher les logs : +- ✅ Tous les logs commençant par `[API SIGNATURE]` +- ✅ Tous les logs commençant par `[SIGNATURE]` +- ✅ Tous les logs commençant par `[SIGNATURE RENDER]` + +### Étape 5 : Vérifier sessionStorage + +**Dans DevTools → Application → Storage → Session Storage** + +Chercher la clé : `docuseal_signature_b64` + +**Attendu** : +- Clé présente +- Valeur commence par "data:image/" +- Valeur ressemble à du base64 + +### Étape 6 : Vérifier le HTML généré + +**Dans DevTools → Elements** + +Chercher l'élément `` + +**Attendu** : +```html + + +``` + +**Vérifier** : +- ✅ Attribut `data-signature` présent +- ✅ Valeur commence par "data:image/" +- ✅ Valeur contient du base64 + +## Problèmes courants + +### Problème 1 : Pas de signature dans la base + +**Symptôme** : +``` +⚠️ [API SIGNATURE] Pas d'org_id sur ce contrat +``` +ou +``` +❌ [API SIGNATURE] Aucune signature trouvée pour cette organisation +``` + +**Solution** : +Ajouter la signature dans la base : +```sql +UPDATE organization_details +SET signature_b64 = 'data:image/png;base64,VOTRE_BASE64' +WHERE org_id = 'ID_ORGANISATION'; +``` + +### Problème 2 : Signature trop longue pour sessionStorage + +**Symptôme** : +``` +❌ [SIGNATURE] Erreur lors du stockage: QuotaExceededError +``` + +**Solution** : +- Compresser l'image de signature +- Utiliser un format plus léger (PNG optimisé) +- Limite sessionStorage : ~5-10MB selon le navigateur + +### Problème 3 : Format de signature incorrect + +**Symptôme** : +DocuSeal affiche une erreur ou ne pré-remplit pas + +**Solution** : +Vérifier le format : +- ✅ CORRECT : `data:image/png;base64,iVBORw0KGgo...` +- ❌ INCORRECT : `iVBORw0KGgo...` (manque le préfixe) +- ❌ INCORRECT : `data:image/png,base64,iVBORw0KGgo...` (virgule au mauvais endroit) + +### Problème 4 : Le composant se re-render avant le stockage + +**Symptôme** : +``` +🎨 [SIGNATURE RENDER] Signature depuis sessionStorage: { exists: false } +``` +mais plus tôt on a vu : +``` +✅ [SIGNATURE] Signature stockée avec succès +``` + +**Solution** : +Problème de timing React. Le sessionStorage est lu lors du premier rendu avant le stockage. + +**Fix** : Forcer un re-render après le stockage : +```tsx +if (signatureB64) { + sessionStorage.setItem('docuseal_signature_b64', signatureB64); + // Forcer un re-render + setEmbedSrc(null); + setTimeout(() => setEmbedSrc(embed), 0); +} +``` + +## Commandes utiles + +### Vérifier toutes les organisations avec signature + +```sql +SELECT + o.id, + o.name, + od.structure, + CASE + WHEN od.signature_b64 IS NOT NULL THEN '✅ Signature présente' + ELSE '❌ Pas de signature' + END as statut, + LENGTH(od.signature_b64) as taille +FROM organizations o +LEFT JOIN organization_details od ON od.org_id = o.id +ORDER BY od.signature_b64 IS NOT NULL DESC, o.name; +``` + +### Vérifier les contrats prêts pour le pré-remplissage + +```sql +SELECT + c.id, + c.contract_number, + c.org_id, + o.name as organization_name, + c.signature_status, + CASE + WHEN od.signature_b64 IS NOT NULL THEN '✅ Signature dispo' + ELSE '❌ Pas de signature' + END as signature_employeur +FROM cddu_contracts c +LEFT JOIN organizations o ON o.id = c.org_id +LEFT JOIN organization_details od ON od.org_id = c.org_id +WHERE c.signature_status IN ('En attente', 'Signé par l''employeur') +ORDER BY c.created_at DESC +LIMIT 20; +``` + +## Contact + +Si les logs montrent toutes les étapes OK mais que ça ne fonctionne toujours pas : +1. Copier tous les logs de la console +2. Faire une capture d'écran de l'élément `` dans DevTools +3. Vérifier la version de DocuSeal utilisée +4. Consulter la documentation DocuSeal pour `data-signature` diff --git a/DOCUSEAL_FIXES.md b/DOCUSEAL_FIXES.md new file mode 100644 index 0000000..a4a02c1 --- /dev/null +++ b/DOCUSEAL_FIXES.md @@ -0,0 +1,145 @@ +# Résumé des Corrections - Signatures DocuSeal + +## Date: 14 octobre 2025 + +## Problème identifié + +❌ **Erreur** : `Unable to find form with slug 'mj9RzzWW2LbvnT?data-allow-typed-signature=false'` + +**Cause** : Les paramètres `data-*` étaient ajoutés à l'URL du slug DocuSeal, ce qui causait une erreur 404. + +## Solution implémentée + +Les attributs `data-*` de DocuSeal doivent être passés comme **attributs HTML** sur le composant ``, **pas dans l'URL** `data-src`. + +### ✅ Modifications effectuées + +#### 1. Composant `` + +**Fichiers modifiés** : +- `/app/(app)/contrats/[id]/page.tsx` +- `/app/(app)/signatures-electroniques/page.tsx` + +**Avant** (❌ incorrect) : +```tsx + + +``` + +**Après** (✅ correct) : +```tsx + + +``` + +#### 2. Génération des URLs propres + +**Fichiers modifiés** : +- `/app/(app)/contrats/[id]/page.tsx` (lignes ~703-710, ~736-744) +- `/app/(app)/signatures-electroniques/page.tsx` (lignes ~203-218, ~243-251) +- `/app/api/docuseal-signature/route.ts` (lignes ~225-233) + +**Changement** : Les URLs DocuSeal sont maintenant générées **sans paramètres de requête**. + +```typescript +// ✅ Correct +embed = `https://docuseal.eu/s/${docusealId}`; + +// ❌ Ancien (incorrect) +embed = `https://docuseal.eu/s/${docusealId}?data-allow-typed-signature=false`; +``` + +## État actuel de la fonctionnalité + +### ✅ Désactivation de la saisie manuelle + +**Statut** : Implémenté et fonctionnel + +L'attribut `data-allow-typed-signature="false"` est ajouté sur le composant `` dans : +- Page de détail d'un contrat (`/contrats/[id]`) +- Page des signatures électroniques (`/signatures-electroniques`) + +### ⏳ Pré-remplissage de la signature (EN COURS) + +**Statut** : Implémentation partielle + +**Ce qui fonctionne** : +- ✅ Colonne `signature_b64` créée dans `organization_details` +- ✅ L'API récupère la signature depuis la base de données +- ✅ Le paramètre `orgId` est passé depuis les composants + +**Ce qui reste à faire** : +- [ ] Ajouter `data-signature` dynamiquement sur `` +- [ ] Récupérer la signature côté client ou la passer via le serveur +- [ ] Tester avec une vraie signature base64 + +## Comment tester + +### Test 1 : Désactivation de la saisie manuelle + +1. Créer une nouvelle signature électronique depuis : + - `/staff/contrats/[id]` (bouton "Lancer signature électronique") + - Ou `/staff/contrats` (sélection multiple + signature groupée) + +2. Ouvrir le lien de signature (en tant qu'employeur) + +3. **Vérifier** : + - ❌ L'option "Saisir" ne doit PAS être disponible + - ✅ Les options "Dessiner" et "Charger" doivent être disponibles + +### Test 2 : Pré-remplissage de la signature (à venir) + +1. Ajouter une signature dans la base : +```sql +UPDATE organization_details +SET signature_b64 = 'data:image/png;base64,iVBORw0KG...' +WHERE org_id = 'VOTRE_ORG_ID'; +``` + +2. Créer une signature électronique pour un contrat de cette organisation + +3. **Vérifier** : + - La signature devrait être pré-remplie pour l'employeur + - L'employeur peut la valider ou la remplacer + +## Prochaines étapes + +### Pour le pré-remplissage de la signature + +**Option A** : Via l'API DocuSeal (recommandé) +- Utiliser le paramètre `values` lors de la création de la soumission +- Nécessite de connaître le nom exact du champ de signature dans le template + +**Option B** : Côté client +- Récupérer la signature depuis Supabase côté client +- L'injecter dans l'attribut `data-signature` du composant +- Plus complexe mais plus flexible + +**Option C** : URL encode dans signature_link +- Stocker la signature encodée dans le lien de signature +- L'extraire et l'ajouter dynamiquement au composant +- Limite de taille d'URL potentielle + +## Documentation de référence + +- **DocuSeal Embed Documentation** : https://docs.docuseal.co/embed +- **Attributs data-*** : + - `data-allow-typed-signature`: boolean (default: true) + - `data-signature`: string (base64, URL, ou texte) + - `data-language`: string (ex: "fr") + - `data-with-title`: boolean + - `data-background-color`: string + +## Notes importantes + +⚠️ Les attributs `data-*` de DocuSeal sont des **attributs HTML**, pas des paramètres d'URL. + +⚠️ L'URL `data-src` doit pointer vers le slug DocuSeal pur sans query parameters. + +⚠️ Pour le pré-remplissage, le format base64 doit être complet : `data:image/png;base64,...` diff --git a/DOCUSEAL_SIGNATURE_CONFIG.md b/DOCUSEAL_SIGNATURE_CONFIG.md index 830a560..50e2b02 100644 --- a/DOCUSEAL_SIGNATURE_CONFIG.md +++ b/DOCUSEAL_SIGNATURE_CONFIG.md @@ -1,33 +1,81 @@ # Configuration des Signatures Électroniques DocuSeal -## Date: 14 octobre 2025 +## Date: 14 octobre 2025 (Mise à jour) -## Modifications apportées +## Fonctionnalités -### Restriction des méthodes de signature +### 1. Restriction des méthodes de signature -L'API DocuSeal a été configurée pour **désactiver la saisie de signature au clavier** et autoriser uniquement : -- ✅ **Dessin de signature** (`draw_signature: true`) -- ✅ **Chargement de photo** (`upload_signature: true`) -- ❌ **Saisie au clavier** (`type_signature: false`) +L'API DocuSeal a été configurée pour **désactiver la saisie de signature au clavier**. -### Mémorisation des signatures +Selon la documentation DocuSeal : +- `data-allow-typed-signature`: Set `false` to disallow users to type their signature (Default: true) -La mémorisation des signatures est **explicitement activée** (`save_signature: true`) pour garantir que les utilisateurs peuvent réutiliser leur signature lors de signatures ultérieures. -> Note : Ce paramètre est normalement activé par défaut dans DocuSeal, mais nous l'activons explicitement pour plus de sécurité. +Les méthodes disponibles sont : +- ✅ **Dessin de signature** (activé par défaut) +- ✅ **Chargement de photo** (activé par défaut) +- ❌ **Saisie au clavier** (`data-allow-typed-signature=false`) + +**⚠️ Important** : Le paramètre `data-allow-typed-signature` doit être passé dans l'URL de l'embed DocuSeal, pas dans l'API de création de soumission. + +### 2. Mémorisation des signatures + +La mémorisation des signatures est **activée par défaut** dans DocuSeal. +Les utilisateurs peuvent réutiliser automatiquement leur signature lors de signatures ultérieures. + +### 3. Pré-remplissage de la signature employeur (✨ NOUVEAU) + +La signature de l'employeur peut être **pré-remplie automatiquement** depuis la base de données : +- Signature stockée en base64 dans `organization_details.signature_b64` +- Pré-remplissage automatique via le paramètre `data-signature` dans l'URL +- L'employeur peut valider directement ou remplacer la signature + +📖 **Voir documentation complète** : [DOCUSEAL_SIGNATURE_PREFILL.md](./DOCUSEAL_SIGNATURE_PREFILL.md) ## Fichiers modifiés ### `/app/api/docuseal-signature/route.ts` -Ajout des préférences de signature dans la création des soumissions DocuSeal : +**1. Configuration des submitters avec metadata** pour désactiver la saisie au clavier : ```typescript -preferences: { - draw_signature: true, // Activer signature par dessin - type_signature: false, // Désactiver signature par saisie - upload_signature: true, // Activer chargement de photo - save_signature: true // Mémoriser la signature (explicite) +submitters: [ + { + role: 'Employeur', + email: employerEmail, + metadata: { + 'allow-typed-signature': false, // Désactiver signature par saisie + // Pré-remplir la signature si disponible (depuis organization_details) + ...(employerSignatureB64 && { + 'signature': employerSignatureB64 + }) + } + }, + { + role: 'Salarié', + email: employeeEmail, + metadata: { + 'allow-typed-signature': false // Désactiver signature par saisie + } + } +] +``` + +**2. Récupération de la signature depuis la base de données** : + +```typescript +// Récupérer la signature base64 de l'organisation si disponible +let employerSignatureB64 = null; +if (orgId) { + const { data: orgDetails } = await supabase + .from('organization_details') + .select('signature_b64') + .eq('org_id', orgId) + .maybeSingle(); + + if (orgDetails?.signature_b64) { + employerSignatureB64 = orgDetails.signature_b64; + } } ``` @@ -66,10 +114,10 @@ Les configurations s'appliquent automatiquement à : ## Avantages ✅ **Sécurité renforcée** : Évite les signatures trop simplistes par saisie au clavier -✅ **Expérience utilisateur améliorée** : Mémorisation explicite de la signature +✅ **Expérience utilisateur** : Dessin et upload disponibles par défaut ✅ **Conformité légale** : Signatures plus authentiques (dessin/scan) -✅ **Gain de temps** : Réutilisation automatique de la signature mémorisée -✅ **Configuration explicite** : Paramètres définis explicitement pour éviter toute ambiguïté +✅ **Mémorisation native** : DocuSeal sauvegarde automatiquement les signatures +✅ **Configuration correcte** : Utilisation des bons paramètres API selon la documentation ## Test @@ -86,9 +134,10 @@ Pour tester les modifications : ## Références API DocuSeal - Documentation des préférences de signature : https://docs.docuseal.co/api -- Paramètre `preferences` dans la création de soumission -- Options configurées explicitement : - - `draw_signature: true` - Permet de dessiner la signature - - `type_signature: false` - Désactive la saisie au clavier - - `upload_signature: true` - Permet le chargement d'image - - `save_signature: true` - Active explicitement la mémorisation +- Paramètre utilisé : `metadata` dans les submitters +- Options configurées : + - `allow-typed-signature: false` - Désactive la saisie au clavier (Default: true) +- Options par défaut (pas besoin de les configurer) : + - Signature par dessin : activée + - Upload de signature : activé + - Mémorisation : activée diff --git a/DOCUSEAL_SIGNATURE_PREFILL.md b/DOCUSEAL_SIGNATURE_PREFILL.md new file mode 100644 index 0000000..c691908 --- /dev/null +++ b/DOCUSEAL_SIGNATURE_PREFILL.md @@ -0,0 +1,219 @@ +# Pré-remplissage de la Signature Employeur - DocuSeal + +## Date: 14 octobre 2025 + +## Fonctionnalité + +Cette fonctionnalité permet de **pré-remplir automatiquement la signature de l'employeur** lors de la création d'une signature électronique via DocuSeal. + +### Principe + +1. La signature de l'employeur est stockée en **base64** dans la table `organization_details` (colonne `signature_b64`) +2. Lors de la création d'une signature électronique, l'API récupère cette signature +3. DocuSeal pré-remplit le champ de signature avec cette image +4. L'employeur peut : + - Utiliser la signature pré-remplie (gain de temps) + - Ou la remplacer par une nouvelle signature + +## Base de données + +### Table: `organization_details` + +Nouvelle colonne ajoutée : +```sql +ALTER TABLE organization_details +ADD COLUMN signature_b64 TEXT; +``` + +**Format attendu :** +- Type: `TEXT` +- Contenu: Image de signature encodée en base64 +- Format acceptés par DocuSeal : + - Base64 encodée (ex: `data:image/png;base64,iVBORw0KG...`) + - URL publique d'une image + - Texte brut (sera rendu avec une police standard) + +### Comment remplir la signature + +1. **Via l'interface d'administration** (à implémenter) +2. **Via SQL directement** : +```sql +UPDATE organization_details +SET signature_b64 = 'data:image/png;base64,iVBORw0KGgoAAAANS...' +WHERE org_id = 'votre-org-id'; +``` + +## Modifications techniques + +### 1. API DocuSeal Signature (`/app/api/docuseal-signature/route.ts`) + +**Ajout du paramètre `orgId`** : +```typescript +const { + // ... autres paramètres + orgId = null // ID de l'organisation pour récupérer la signature +} = data; +``` + +**Récupération de la signature** : +```typescript +let employerSignatureB64 = null; +if (orgId) { + const { data: orgDetails } = await supabase + .from('organization_details') + .select('signature_b64') + .eq('org_id', orgId) + .maybeSingle(); + + if (orgDetails?.signature_b64) { + employerSignatureB64 = orgDetails.signature_b64; + } +} +``` + +**Pré-remplissage dans DocuSeal** : +```typescript +submitters: [ + { + role: 'Employeur', + email: employerEmail, + metadata: { + 'allow-typed-signature': false, + ...(employerSignatureB64 && { + 'signature': employerSignatureB64 // data-signature pour pré-remplir + }) + } + } +] +``` + +### 2. API Bulk E-Sign (`/app/api/staff/contracts/bulk-esign/route.ts`) + +Ajout de `orgId` dans les données envoyées : +```typescript +const signatureData = { + // ... autres champs + orgId: contract.org_id // Passer l'org_id pour récupérer la signature +}; +``` + +### 3. Contract Editor (`/components/staff/contracts/ContractEditor.tsx`) + +Ajout de `orgId` dans les données de signature individuelle : +```typescript +const signatureData = { + // ... autres champs + orgId: contract.org_id // Passer l'org_id pour récupérer la signature +}; +``` + +## Points d'application + +La signature pré-remplie fonctionne pour : + +1. ✅ **Signature individuelle** depuis `/staff/contrats/[id]` +2. ✅ **Signature groupée** depuis `/staff/contrats` (sélection multiple) + +## Comportement utilisateur + +### Pour l'employeur : + +1. **Si une signature est enregistrée** : + - Le champ de signature s'affiche avec la signature pré-remplie + - L'employeur peut la valider directement (1 clic) + - Ou la remplacer en dessinant/uploadant une nouvelle signature + +2. **Si aucune signature n'est enregistrée** : + - Comportement normal : l'employeur doit créer sa signature + - Options : Dessiner ou Uploader une image + - ❌ Saisie au clavier désactivée + +### Pour le salarié : + +- Pas de pré-remplissage (chaque salarié signe manuellement) +- Options : Dessiner ou Uploader une image +- ❌ Saisie au clavier désactivée + +## Avantages + +✅ **Gain de temps** : L'employeur n'a pas besoin de redessiner à chaque contrat +✅ **Cohérence** : Même signature sur tous les contrats de l'organisation +✅ **Flexibilité** : Possibilité de remplacer la signature si nécessaire +✅ **Sécurité** : Signature stockée de manière centralisée et sécurisée + +## Migration SQL + +```sql +-- Ajouter la colonne signature_b64 si elle n'existe pas +ALTER TABLE organization_details +ADD COLUMN IF NOT EXISTS signature_b64 TEXT; + +-- Créer un index pour les recherches +CREATE INDEX IF NOT EXISTS idx_organization_details_signature +ON organization_details(org_id) +WHERE signature_b64 IS NOT NULL; +``` + +## Test + +### Prérequis + +1. **Ajouter une signature dans la base** : +```sql +UPDATE organization_details +SET signature_b64 = 'data:image/png;base64,iVBORw0KG...' +WHERE org_id = 'VOTRE_ORG_ID'; +``` + +2. **Créer une signature électronique** pour un contrat de cette organisation + +### Processus de test + +1. **Depuis la page du contrat** (`/contrats/[id]`) : + - Ouvrir un contrat appartenant à une organisation avec signature_b64 + - Cliquer sur "Voir signature" ou accéder à la signature électronique + +2. **Vérification** : + - La signature de l'employeur devrait être pré-remplie + - L'employeur peut la valider directement ou la remplacer + - Le salarié n'a pas de pré-remplissage (signature manuelle requise) + +### Fonctionnement technique + +1. **API `/api/contrats/[id]/signature`** : + - Récupère le contrat avec l'org_id + - Récupère la signature_b64 depuis organization_details + - Retourne les données avec la signature + +2. **Page `/contrats/[id]`** : + - Récupère les données via l'API + - Stocke la signature dans sessionStorage + - Injecte `data-signature` dans le composant `` + +3. **Composant ``** : + - Lit la signature depuis sessionStorage + - Ajoute l'attribut `data-signature` dynamiquement + - DocuSeal pré-remplit le champ de signature + +### Points d'application actuels + +✅ **Page de détail du contrat** (`/contrats/[id]`) - IMPLÉMENTÉ +⏳ **Page signatures électroniques** (`/signatures-electroniques`) - À IMPLÉMENTER +⏳ **Signature groupée** (`/staff/contrats`) - À IMPLÉMENTER (nécessite modification de l'API bulk-esign) + +## Références + +- Documentation DocuSeal `data-signature` : https://docs.docuseal.co/api +- Format : Allows pre-filling signature fields +- Types acceptés : + - Base64 encoded image string + - Public URL to an image + - Plain text (rendered with standard font) + +## TODO (Améliorations futures) + +- [ ] Interface d'administration pour uploader/gérer la signature +- [ ] Validation du format base64 +- [ ] Prévisualisation de la signature dans l'interface +- [ ] Historique des signatures utilisées +- [ ] Support de plusieurs signatures par organisation (différents signataires) diff --git a/app/(app)/contrats/[id]/page.tsx b/app/(app)/contrats/[id]/page.tsx index e3365a6..8ea97da 100644 --- a/app/(app)/contrats/[id]/page.tsx +++ b/app/(app)/contrats/[id]/page.tsx @@ -679,6 +679,8 @@ export default function ContratPage() { const result = await response.json(); console.log('📋 [SIGNATURE] Données contrat depuis API:', result); + console.log('🔍 [SIGNATURE] result.data:', result.data); + console.log('🔍 [SIGNATURE] result.data.signature_b64:', result.data?.signature_b64); if (!result.success || !result.data) { setIsLoadingSignature(false); @@ -687,6 +689,22 @@ export default function ContratPage() { } const contractData = result.data; + console.log('📦 [SIGNATURE] contractData extrait:', contractData); + + // Stocker la signature si disponible + const signatureB64 = contractData.signature_b64; + console.log('🖊️ [SIGNATURE] signatureB64 extraite:', { + exists: !!signatureB64, + type: typeof signatureB64, + length: signatureB64?.length, + preview: signatureB64?.substring(0, 50) + }); + + if (signatureB64) { + console.log('✅ [SIGNATURE] Signature B64 disponible, longueur:', signatureB64.length); + } else { + console.log('⚠️ [SIGNATURE] Aucune signature B64 disponible'); + } // Vérifier si la soumission DocuSeal n'a pas été créée if (!contractData.docuseal_submission_id && contractData.signature_status === "Non initiée") { @@ -703,6 +721,8 @@ export default function ContratPage() { const signatureLinkMatch = contractData.signature_link.match(/docuseal_id=([^&]+)/); if (signatureLinkMatch) { const docusealId = signatureLinkMatch[1]; + + // L'URL doit être propre sans paramètres embed = `https://docuseal.eu/s/${docusealId}`; console.log('🔗 [SIGNATURE] URL embed depuis signature_link:', embed); } @@ -733,6 +753,7 @@ export default function ContratPage() { const employer = roles.find((r: any) => (r.role || r.name) === 'Employeur') || {}; if (employer?.slug) { + // URL propre sans paramètres embed = `https://docuseal.eu/s/${employer.slug}`; console.log('🔗 [SIGNATURE] URL embed depuis DocuSeal API:', embed); } else { @@ -749,6 +770,31 @@ export default function ContratPage() { console.log('✅ [SIGNATURE] URL embed trouvée:', embed); setEmbedSrc(embed); + // Stocker la signature pour l'ajouter au composant + console.log('🔍 [SIGNATURE] Tentative de stockage de la signature...'); + console.log('🔍 [SIGNATURE] signatureB64 value:', signatureB64); + + if (signatureB64) { + console.log('📝 [SIGNATURE] Stockage de la signature dans sessionStorage'); + console.log('📝 [SIGNATURE] Longueur de la signature:', signatureB64.length); + console.log('📝 [SIGNATURE] Preview:', signatureB64.substring(0, 100)); + + try { + sessionStorage.setItem('docuseal_signature_b64', signatureB64); + const stored = sessionStorage.getItem('docuseal_signature_b64'); + console.log('✅ [SIGNATURE] Signature stockée avec succès, vérification:', { + stored: !!stored, + length: stored?.length, + matches: stored === signatureB64 + }); + } catch (e) { + console.error('❌ [SIGNATURE] Erreur lors du stockage:', e); + } + } else { + console.log('⚠️ [SIGNATURE] Pas de signature à stocker, nettoyage sessionStorage'); + sessionStorage.removeItem('docuseal_signature_b64'); + } + // Masquer la modale de chargement setIsLoadingSignature(false); @@ -1493,12 +1539,42 @@ return (
{embedSrc ? (
{ + console.log('🎨 [SIGNATURE RENDER] Génération du HTML docuseal-form'); + console.log('🎨 [SIGNATURE RENDER] embedSrc:', embedSrc); + + // Récupérer la signature depuis sessionStorage + const signatureB64 = typeof window !== 'undefined' ? sessionStorage.getItem('docuseal_signature_b64') : null; + + console.log('🎨 [SIGNATURE RENDER] Signature depuis sessionStorage:', { + exists: !!signatureB64, + length: signatureB64?.length, + preview: signatureB64?.substring(0, 50) + }); + + const signatureAttr = signatureB64 ? `data-signature="${signatureB64.replace(/"/g, '"')}"` : ''; + + console.log('🎨 [SIGNATURE RENDER] signatureAttr généré:', { + hasAttr: !!signatureAttr, + attrLength: signatureAttr.length, + attrPreview: signatureAttr.substring(0, 100) + }); + + const html = ` - ` + data-background-color="#fff" + data-allow-typed-signature="false" + ${signatureAttr}> + `; + + console.log('🎨 [SIGNATURE RENDER] HTML final généré'); + console.log('🎨 [SIGNATURE RENDER] HTML length:', html.length); + console.log('🎨 [SIGNATURE RENDER] HTML contient data-signature:', html.includes('data-signature=')); + + return html; + })() }} /> ) : (
Préparation du formulaire…
diff --git a/app/(app)/signatures-electroniques/page.tsx b/app/(app)/signatures-electroniques/page.tsx index a7f6354..563b175 100644 --- a/app/(app)/signatures-electroniques/page.tsx +++ b/app/(app)/signatures-electroniques/page.tsx @@ -193,6 +193,16 @@ export default function SignaturesElectroniques() { console.log('🔍 [SIGNATURES] Debug - record fields:', f); console.log('🔍 [SIGNATURES] Debug - embed_src_employeur:', f.embed_src_employeur); console.log('🔍 [SIGNATURES] Debug - docuseal_template_id:', f.docuseal_template_id); + console.log('🔍 [SIGNATURES] Debug - signature_b64:', f.signature_b64 ? 'présente' : 'absente'); + + // Gérer la signature pré-remplie si disponible + if (f.signature_b64) { + console.log('✅ [SIGNATURES] Signature trouvée, stockage dans sessionStorage'); + sessionStorage.setItem('docuseal_signature_b64', f.signature_b64); + } else { + console.log('⚠️ [SIGNATURES] Pas de signature dans les données'); + sessionStorage.removeItem('docuseal_signature_b64'); + } // 1) Si l'URL d'embed est déjà en base (signature_link) if (typeof f.embed_src_employeur === 'string' && f.embed_src_employeur.trim()) { @@ -203,6 +213,8 @@ export default function SignaturesElectroniques() { const signatureLinkMatch = signatureLink.match(/docuseal_id=([^&]+)/); if (signatureLinkMatch) { const docusealId = signatureLinkMatch[1]; + + // URL propre sans paramètres embed = `https://docuseal.eu/s/${docusealId}`; console.log('🔗 [SIGNATURES] URL embed depuis signature_link:', embed); } else { @@ -240,6 +252,7 @@ export default function SignaturesElectroniques() { const employer = roles.find((r: any) => (r.role || r.name) === 'Employeur') || {}; if (employer?.slug) { + // URL propre sans paramètres embed = `https://docuseal.eu/s/${employer.slug}`; console.log('🔗 [SIGNATURES] URL embed depuis DocuSeal API (slug):', embed); } else { @@ -557,14 +570,33 @@ export default function SignaturesElectroniques() {
{embedSrc ? ( -
- ` - }} /> +
{ + if (el && embedSrc) { + // Récupérer la signature depuis sessionStorage + const signature = sessionStorage.getItem('docuseal_signature_b64'); + + // Construire les attributs + let dataSignatureAttr = ''; + if (signature) { + console.log('✅ [SIGNATURES] Injection de la signature dans DocuSeal form'); + dataSignatureAttr = `data-signature="${signature}"`; + } else { + console.log('⚠️ [SIGNATURES] Pas de signature à injecter'); + } + + // Injecter le HTML avec ou sans signature + el.innerHTML = ` + `; + } + }} + /> ) : (
Préparation du formulaire…
)} diff --git a/app/api/contrats/[id]/signature/route.ts b/app/api/contrats/[id]/signature/route.ts index 5e80b47..d17a8be 100644 --- a/app/api/contrats/[id]/signature/route.ts +++ b/app/api/contrats/[id]/signature/route.ts @@ -12,10 +12,17 @@ export async function GET( // Utiliser le service role pour contourner RLS const supabase = createSbServiceRole(); - // Récupérer le contrat avec les informations de signature + // Récupérer le contrat avec les informations de signature et l'org_id const { data: contractData, error } = await supabase .from('cddu_contracts') - .select('id, docuseal_template_id, docuseal_submission_id, signature_link, signature_status') + .select(` + id, + docuseal_template_id, + docuseal_submission_id, + signature_link, + signature_status, + org_id + `) .eq('id', contractId) .single(); @@ -26,10 +33,54 @@ export async function GET( console.log('✅ [API SIGNATURE] Contrat trouvé:', contractData); - return NextResponse.json({ + // Récupérer la signature de l'organisation si disponible + let signatureB64 = null; + if (contractData.org_id) { + console.log('🔍 [API SIGNATURE] org_id présent:', contractData.org_id); + console.log('🔍 [API SIGNATURE] Recherche de signature pour org_id...'); + + const { data: orgDetails, error: orgError } = await supabase + .from('organization_details') + .select('signature_b64') + .eq('org_id', contractData.org_id) + .maybeSingle(); + + console.log('📋 [API SIGNATURE] Résultat requête organization_details:', { + hasData: !!orgDetails, + hasError: !!orgError, + error: orgError, + hasSignature: !!orgDetails?.signature_b64, + signatureLength: orgDetails?.signature_b64?.length, + signaturePreview: orgDetails?.signature_b64?.substring(0, 50) + }); + + if (orgDetails?.signature_b64) { + signatureB64 = orgDetails.signature_b64; + console.log('✅ [API SIGNATURE] Signature trouvée pour l\'organisation'); + console.log('📏 [API SIGNATURE] Longueur de la signature:', signatureB64.length); + } else { + console.log('❌ [API SIGNATURE] Aucune signature trouvée pour cette organisation'); + } + } else { + console.log('⚠️ [API SIGNATURE] Pas d\'org_id sur ce contrat'); + } + + const response = { success: true, - data: contractData + data: { + ...contractData, + signature_b64: signatureB64 + } + }; + + console.log('📤 [API SIGNATURE] Réponse finale:', { + hasSignature: !!signatureB64, + signatureLength: signatureB64?.length, + contractId: contractData.id, + orgId: contractData.org_id }); + + return NextResponse.json(response); } catch (error) { console.error('❌ [API SIGNATURE] Erreur:', error); diff --git a/app/api/docuseal-signature/route.ts b/app/api/docuseal-signature/route.ts index 1308d49..6ad143e 100644 --- a/app/api/docuseal-signature/route.ts +++ b/app/api/docuseal-signature/route.ts @@ -53,7 +53,8 @@ export async function POST(request: NextRequest) { employeeFirstName, matricule, contractType = 'CDDU', - skipEmployerEmail = false // Nouveau paramètre pour éviter l'envoi d'email à l'employeur + skipEmployerEmail = false, // Nouveau paramètre pour éviter l'envoi d'email à l'employeur + orgId = null // ID de l'organisation pour récupérer la signature } = data; // Validation des champs requis @@ -64,6 +65,24 @@ export async function POST(request: NextRequest) { ); } + // Récupérer la signature base64 de l'organisation si disponible + let employerSignatureB64 = null; + if (orgId) { + console.log('🔍 [SIGNATURE] Recherche de la signature pour org_id:', orgId); + const { data: orgDetails } = await supabase + .from('organization_details') + .select('signature_b64') + .eq('org_id', orgId) + .maybeSingle(); + + if (orgDetails?.signature_b64) { + employerSignatureB64 = orgDetails.signature_b64; + console.log('✅ [SIGNATURE] Signature trouvée pour l\'employeur'); + } else { + console.log('ℹ️ [SIGNATURE] Aucune signature trouvée pour l\'employeur'); + } + } + // Extraire le submission_id depuis le nom du fichier PDF // pdfS3Key format: "unsigned-contracts/contrat_cddu_REFERENCE.pdf" // submission_id attendu: "contrats/contrat_cddu_REFERENCE" (format pour DynamoDB) @@ -149,38 +168,36 @@ export async function POST(request: NextRequest) { console.log('Template DocuSeal créé:', templateResponse.data); // Étape 4 : Création de la soumission DocuSeal - const submissionResponse = await axios.post(`${ENV.DOCUSEAL_API_BASE}/submissions`, { + console.log('🔧 [DOCUSEAL] Création de la soumission avec:', { + templateId, + hasEmployerSignature: !!employerSignatureB64, + signaturePreview: employerSignatureB64 ? employerSignatureB64.substring(0, 50) + '...' : 'N/A' + }); + + const submissionPayload = { template_id: templateId, send_email: false, - preferences: { - draw_signature: true, // Activer signature par dessin - type_signature: false, // Désactiver signature par saisie - upload_signature: true, // Activer chargement de photo - save_signature: true // Mémoriser la signature (explicite) - }, submitters: [ { role: 'Employeur', email: employerEmail, - preferences: { - draw_signature: true, - type_signature: false, - upload_signature: true, - save_signature: true - } + // Pré-remplir la signature de l'employeur si disponible + ...(employerSignatureB64 && { + values: { + signature: employerSignatureB64 + } + }) }, { role: 'Salarié', - email: employeeEmail, - preferences: { - draw_signature: true, - type_signature: false, - upload_signature: true, - save_signature: true - } + email: employeeEmail } ] - }, { + }; + + console.log('📤 [DOCUSEAL] Payload complet:', JSON.stringify(submissionPayload, null, 2)); + + const submissionResponse = await axios.post(`${ENV.DOCUSEAL_API_BASE}/submissions`, submissionPayload, { headers: { 'X-Auth-Token': ENV.DOCUSEAL_TOKEN, 'Content-Type': 'application/json' @@ -188,6 +205,9 @@ export async function POST(request: NextRequest) { }); console.log('Soumission DocuSeal créée:', submissionResponse.data); + if (employerSignatureB64) { + console.log('✅ [SIGNATURE] Signature employeur pré-remplie'); + } // Récupérer le submission_id DocuSeal const docusealSubmissionId = submissionResponse.data[0].submission_id; @@ -205,7 +225,14 @@ export async function POST(request: NextRequest) { // Récupérer les informations pour le lien de signature const employerSubmission = submissionResponse.data.find((sub: any) => sub.role === 'Employeur'); const embedCode = employerSubmission.slug; + + // Construire l'URL propre sans paramètres (les data-* sont ajoutés sur le composant HTML) const signatureLink = `https://staging.paie.odentas.fr/odentas-sign?docuseal_id=${embedCode}`; + + console.log('🔗 [SIGNATURE] Lien généré:', signatureLink); + if (employerSignatureB64) { + console.log('✅ [SIGNATURE] Signature B64 disponible pour pré-remplissage côté client'); + } // Étape 5 : Envoi de l'email de signature via le template universel (employeur) // Seulement si skipEmployerEmail est false (signature individuelle) diff --git a/app/api/signatures-electroniques/contrats/route.ts b/app/api/signatures-electroniques/contrats/route.ts index cf0e8f0..2555650 100644 --- a/app/api/signatures-electroniques/contrats/route.ts +++ b/app/api/signatures-electroniques/contrats/route.ts @@ -119,33 +119,57 @@ export async function GET(req: NextRequest) { } // Adapter le format pour correspondre à l'attente de la page (format Airtable-like) - const records = (contracts || []).map(contract => ({ - id: contract.id, - fields: { - 'Reference': contract.reference || contract.contract_number, - 'reference': contract.reference || contract.contract_number, - 'Nom salarié': contract.employee_name, - 'employee_name': contract.employee_name, - 'Prénom Salarié': contract.employee_name, // Full name (pas de séparation prénom/nom) - 'Nom Salarié': contract.employee_name, - 'Matricule API': contract.employee_matricule, - 'Production': contract.production_name, - 'role': contract.role, - 'Profession': contract.role, - 'start_date': contract.start_date, - 'Date de début': contract.start_date, - 'État de la demande': contract.etat_de_la_demande || 'Traitée', - 'Contrat signé par employeur': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe_par_employeur) ? 'Oui' : 'Non', - 'Contrat signé': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe) ? 'Oui' : 'Non', - 'signature_employeur': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe_par_employeur), - 'employer_signed': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe_par_employeur), - 'signature_salarie': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe), - 'employee_signed': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe), - 'docuseal_template_id': contract.docuseal_template_id, - 'docuseal_submission_id': contract.docuseal_submission_id, - 'signature_link': contract.signature_link, - 'embed_src_employeur': contract.signature_link + // et récupérer les signatures des organisations + const records = await Promise.all((contracts || []).map(async (contract) => { + // Récupérer la signature de l'organisation si disponible + let signatureB64 = null; + if (contract.org_id) { + try { + const { data: orgDetails } = await sb + .from('organization_details') + .select('signature_b64') + .eq('org_id', contract.org_id) + .maybeSingle(); + + if (orgDetails?.signature_b64) { + signatureB64 = orgDetails.signature_b64; + console.log(`✅ [signatures-electroniques] Signature trouvée pour org_id: ${contract.org_id}`); + } + } catch (err) { + console.warn(`⚠️ [signatures-electroniques] Erreur récupération signature pour org_id: ${contract.org_id}`, err); + } } + + return { + id: contract.id, + fields: { + 'Reference': contract.reference || contract.contract_number, + 'reference': contract.reference || contract.contract_number, + 'Nom salarié': contract.employee_name, + 'employee_name': contract.employee_name, + 'Prénom Salarié': contract.employee_name, // Full name (pas de séparation prénom/nom) + 'Nom Salarié': contract.employee_name, + 'Matricule API': contract.employee_matricule, + 'Production': contract.production_name, + 'role': contract.role, + 'Profession': contract.role, + 'start_date': contract.start_date, + 'Date de début': contract.start_date, + 'État de la demande': contract.etat_de_la_demande || 'Traitée', + 'Contrat signé par employeur': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe_par_employeur) ? 'Oui' : 'Non', + 'Contrat signé': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe) ? 'Oui' : 'Non', + 'signature_employeur': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe_par_employeur), + 'employer_signed': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe_par_employeur), + 'signature_salarie': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe), + 'employee_signed': ['oui', 'Oui', 'OUI', 'true', true].includes(contract.contrat_signe), + 'docuseal_template_id': contract.docuseal_template_id, + 'docuseal_submission_id': contract.docuseal_submission_id, + 'signature_link': contract.signature_link, + 'embed_src_employeur': contract.signature_link, + 'org_id': contract.org_id, + 'signature_b64': signatureB64 + } + }; })); return NextResponse.json({ records }); diff --git a/app/api/staff/contracts/bulk-esign/route.ts b/app/api/staff/contracts/bulk-esign/route.ts index a4c9e20..415e0c0 100644 --- a/app/api/staff/contracts/bulk-esign/route.ts +++ b/app/api/staff/contracts/bulk-esign/route.ts @@ -141,7 +141,8 @@ export async function POST(request: NextRequest) { employeeFirstName: contract.employee_name?.split(' ')[0], matricule: contract.employee_matricule, contractType: 'CDDU', - skipEmployerEmail: true // Ne pas envoyer d'email à l'employeur individuellement + skipEmployerEmail: true, // Ne pas envoyer d'email à l'employeur individuellement + orgId: contract.org_id // Passer l'org_id pour récupérer la signature }; console.log(`📤 Appel API DocuSeal pour contrat ${contract.contract_number}`); diff --git a/components/staff/contracts/ContractEditor.tsx b/components/staff/contracts/ContractEditor.tsx index 26a11eb..18ad348 100644 --- a/components/staff/contracts/ContractEditor.tsx +++ b/components/staff/contracts/ContractEditor.tsx @@ -953,7 +953,8 @@ export default function ContractEditor({ employerCode: employerCode, employeeFirstName: salarie?.prenom || contract.employee_name?.split(' ')[0], matricule: salarie?.code_salarie || salarie?.matricule || contract.employee_matricule, - contractType: 'CDDU' + contractType: 'CDDU', + orgId: contract.org_id // Passer l'org_id pour récupérer la signature }; console.log("📤 [SIGNATURE] Données envoyées:", signatureData); diff --git a/supabase/migrations/add_signature_b64_column.sql b/supabase/migrations/add_signature_b64_column.sql new file mode 100644 index 0000000..34e1078 --- /dev/null +++ b/supabase/migrations/add_signature_b64_column.sql @@ -0,0 +1,32 @@ +-- Migration: Ajouter la colonne signature_b64 dans organization_details +-- Date: 2025-10-14 +-- Description: Permet de stocker la signature de l'employeur en base64 pour pré-remplissage DocuSeal + +-- Ajouter la colonne signature_b64 si elle n'existe pas +ALTER TABLE organization_details +ADD COLUMN IF NOT EXISTS signature_b64 TEXT; + +-- Créer un index pour optimiser les recherches +CREATE INDEX IF NOT EXISTS idx_organization_details_signature +ON organization_details(org_id) +WHERE signature_b64 IS NOT NULL; + +-- Ajouter un commentaire sur la colonne +COMMENT ON COLUMN organization_details.signature_b64 IS +'Signature de l''employeur en base64 pour pré-remplissage DocuSeal. Format acceptés: base64, URL publique, ou texte brut.'; + +-- Exemple d'insertion (à adapter avec une vraie signature) +-- UPDATE organization_details +-- SET signature_b64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...' +-- WHERE org_id = 'votre-org-id'; + +-- Vérification +SELECT + org_id, + structure, + CASE + WHEN signature_b64 IS NOT NULL THEN 'Signature présente' + ELSE 'Pas de signature' + END as statut_signature +FROM organization_details +ORDER BY structure;