diff --git a/SIGNATURE_STATE_REFACTORING.md b/SIGNATURE_STATE_REFACTORING.md new file mode 100644 index 0000000..0a6e673 --- /dev/null +++ b/SIGNATURE_STATE_REFACTORING.md @@ -0,0 +1,162 @@ +# Refactorisation : Suppression de la dépendance à sessionStorage pour les signatures + +## 📋 Résumé + +Suppression de l'utilisation de `sessionStorage` pour stocker les signatures électroniques. Les signatures sont maintenant gérées via l'état React, éliminant les dépendances inter-pages et les problèmes de synchronisation. + +## 🔧 Changements techniques + +### 1. Page signatures électroniques (`app/(app)/signatures-electroniques/page.tsx`) + +#### Avant +```typescript +// Stockage dans sessionStorage +sessionStorage.setItem('docuseal_signature_b64', normalizedSignature); + +// Lecture depuis sessionStorage +const signature = sessionStorage.getItem('docuseal_signature_b64'); +``` + +#### Après +```typescript +// État React local +const [activeSignature, setActiveSignature] = useState(null); + +// Mise à jour de l'état +setActiveSignature(normalizedSignature); + +// Utilisation directe de l'état +const signature = activeSignature; +``` + +**Fichier modifié :** +- Ajout de l'état `activeSignature` (ligne ~119) +- Remplacement de `sessionStorage.setItem()` par `setActiveSignature()` (ligne ~524) +- Remplacement de `sessionStorage.getItem()` par lecture de `activeSignature` (ligne ~444) +- Ajout d'`activeSignature` dans les dépendances du useEffect (ligne ~465) + +### 2. Page contrats (`app/(app)/contrats/[id]/page.tsx`) + +#### Avant +```typescript +// Écriture dans sessionStorage +sessionStorage.setItem('docuseal_signature_b64', signatureB64); + +// Lecture depuis sessionStorage dans le rendu +const signatureB64 = sessionStorage.getItem('docuseal_signature_b64'); +``` + +#### Après +```typescript +// État React +const [signatureB64ForDocuSeal, setSignatureB64ForDocuSeal] = useState(null); + +// Mise à jour lors du chargement +setSignatureB64ForDocuSeal(signatureB64); + +// Utilisation directe dans le rendu +const signatureB64 = signatureB64ForDocuSeal; +``` + +**Fichier modifié :** +- Ajout de l'état `signatureB64ForDocuSeal` (ligne ~586) +- Remplacement de `sessionStorage.setItem()` par `setSignatureB64ForDocuSeal()` (ligne ~780) +- Remplacement de `sessionStorage.getItem()` par lecture de `signatureB64ForDocuSeal` (ligne ~1544) + +## ✅ Avantages + +### 1. **Isolation des composants** +- Chaque page gère sa propre signature indépendamment +- Plus de dépendance inter-pages cachée +- Code plus prévisible et maintenable + +### 2. **Chargement dynamique** +- La signature est chargée directement depuis l'API `/api/contrats/[id]/signature` +- Plus besoin de passer par une autre page pour "initialiser" la signature +- Fonctionne même si l'utilisateur arrive directement sur la page contrat + +### 3. **Réactivité améliorée** +- Les changements de signature se propagent automatiquement via React +- Re-render automatique quand la signature change +- Meilleure intégration avec l'écosystème React + +### 4. **Débogage simplifié** +- L'état est visible dans React DevTools +- Plus de problèmes de synchronisation avec sessionStorage +- Logs plus clairs (état React vs storage navigateur) + +### 5. **Sécurité** +- Les données ne persistent pas entre sessions +- Pas de risque de lecture de signatures par d'autres onglets +- Nettoyage automatique à la fermeture de la page + +## 🔍 Flux de données + +### Page signatures-electroniques +``` +1. Utilisateur ouvre un formulaire DocuSeal +2. openSignature() charge les données du contrat depuis l'API +3. La signature est extraite et stockée dans activeSignature +4. useEffect détecte le changement d'activeSignature +5. Re-render du DocuSeal form avec data-signature mis à jour +``` + +### Page contrats/[id] +``` +1. Utilisateur clique sur "Signature électronique" +2. openSignature() appelle /api/contrats/[id]/signature +3. L'API joint automatiquement signature_b64 depuis organization_details +4. La signature est stockée dans signatureB64ForDocuSeal +5. DocuSeal form est rendu avec data-signature pré-rempli +``` + +## 📦 API impliquée + +### `/api/contrats/[id]/signature` +- Récupère le contrat et joint automatiquement la signature de l'organisation +- Requête sur `organization_details` avec `org_id` du contrat +- Retourne `signature_b64` dans la réponse + +### `/api/signatures-electroniques/contrats` +- Liste les contrats disponibles pour signature +- Enrichit chaque contrat avec `signature_b64` de son organisation + +## 🧪 Tests recommandés + +1. **Isolation** : Ouvrir un contrat directement par URL → signature doit être pré-remplie +2. **Mise à jour** : Modifier une signature → devrait être visible immédiatement +3. **Staff** : En tant que staff, changer d'organisation → nouvelle signature chargée +4. **Sans signature** : Organisation sans signature → pas de pré-remplissage (normal) +5. **Suppression** : Supprimer une signature → formulaires futurs sans pré-remplissage + +## 🔄 Migration + +### Pour les utilisateurs +Aucun impact visible. Le comportement reste identique, mais plus fiable. + +### Pour les développeurs +Si vous avez du code qui : +- Lit `sessionStorage.getItem('docuseal_signature_b64')` → Utiliser l'état React approprié +- Écrit dans sessionStorage pour les signatures → Utiliser setState + +## 📝 Notes techniques + +- Le format de signature reste inchangé : `data:image/png;base64,...` +- La fonction `normalizeSignatureFormat()` assure la compatibilité avec les anciennes données +- Les logs de debug contiennent toujours "sessionStorage" dans les messages pour clarté historique + +## 🎯 Prochaines étapes possibles + +1. Créer un hook personnalisé `useOrganizationSignature(orgId)` pour centraliser la logique +2. Implémenter un cache React Query pour éviter les rechargements +3. Ajouter un indicateur visuel de chargement pendant la récupération de la signature +4. Précharger les signatures des organisations fréquemment utilisées + +--- + +**Date de refactorisation** : 14 octobre 2025 +**Fichiers modifiés** : +- `app/(app)/signatures-electroniques/page.tsx` +- `app/(app)/contrats/[id]/page.tsx` + +**Compatibilité** : ✅ Rétrocompatible (les anciennes signatures en base64 pur fonctionnent toujours) diff --git a/app/(app)/contrats/[id]/page.tsx b/app/(app)/contrats/[id]/page.tsx index 8ea97da..de313b2 100644 --- a/app/(app)/contrats/[id]/page.tsx +++ b/app/(app)/contrats/[id]/page.tsx @@ -582,6 +582,7 @@ export default function ContratPage() { // State pour la modale de signature DocuSeal const [embedSrc, setEmbedSrc] = useState(""); const [modalTitle, setModalTitle] = useState(""); + const [signatureB64ForDocuSeal, setSignatureB64ForDocuSeal] = useState(null); // Signature pour pré-remplissage // State pour la modale de chargement const [isLoadingSignature, setIsLoadingSignature] = useState(false); @@ -770,29 +771,16 @@ 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...'); + // Stocker la signature dans l'etat React pour l'ajouter au composant + console.log('🔍 [SIGNATURE] Stockage de la signature dans l\'etat React...'); 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); - } + console.log('✅ [SIGNATURE] Signature B64 disponible pour pre-remplissage'); + setSignatureB64ForDocuSeal(signatureB64); } else { - console.log('⚠️ [SIGNATURE] Pas de signature à stocker, nettoyage sessionStorage'); - sessionStorage.removeItem('docuseal_signature_b64'); + console.log('⚠️ [SIGNATURE] Aucune signature B64 disponible'); + setSignatureB64ForDocuSeal(null); } // Masquer la modale de chargement @@ -1543,10 +1531,10 @@ return ( 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; + // Utiliser la signature depuis l'etat React au lieu de sessionStorage + const signatureB64 = signatureB64ForDocuSeal; - console.log('🎨 [SIGNATURE RENDER] Signature depuis sessionStorage:', { + console.log('🎨 [SIGNATURE RENDER] Signature depuis l\'etat React:', { exists: !!signatureB64, length: signatureB64?.length, preview: signatureB64?.substring(0, 50) diff --git a/app/(app)/signatures-electroniques/page.tsx b/app/(app)/signatures-electroniques/page.tsx index 9cd1b47..7b165a2 100644 --- a/app/(app)/signatures-electroniques/page.tsx +++ b/app/(app)/signatures-electroniques/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { FileSignature, BellRing, XCircle } from 'lucide-react'; +import { FileSignature, BellRing, XCircle, Sparkles, CheckCircle2, AlertCircle, Info, Lightbulb } from 'lucide-react'; import Script from 'next/script'; import { usePageTitle } from '@/hooks/usePageTitle'; import { useQuery } from '@tanstack/react-query'; @@ -85,6 +85,7 @@ export default function SignaturesElectroniques() { const [contrats, setContrats] = useState([]); const [loading, setLoading] = useState(true); + const [reloadingAfterSignatureChange, setReloadingAfterSignatureChange] = useState(false); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [isOnline, setIsOnline] = useState(true); @@ -109,12 +110,305 @@ export default function SignaturesElectroniques() { // Référence pour le conteneur DocuSeal const docusealContainerRef = useRef(null); + // États pour la gestion de la signature + const [showSignatureModal, setShowSignatureModal] = useState(false); + const [currentSignature, setCurrentSignature] = useState(null); + const [uploadingSignature, setUploadingSignature] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [uploadMessage, setUploadMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [activeSignature, setActiveSignature] = useState(null); // Signature pour DocuSeal (remplace sessionStorage) + const fileInputRef = useRef(null); + // Charger les infos utilisateur et organisations const { data: userInfo } = useUserInfo(); const { data: organizations } = useOrganizations(); // Suppression de pollActive et pollTimer car le polling a été retiré + // Normaliser le format de la signature (ajouter le préfixe si nécessaire) + function normalizeSignatureFormat(signature: string | null): string | null { + if (!signature) return null; + + // Si déjà au bon format (data:image/...), retourner tel quel + if (signature.startsWith('data:image/')) { + return signature; + } + + // Sinon, ajouter le préfixe PNG + return `data:image/png;base64,${signature}`; + } + + // Charger la signature de l'organisation + async function loadSignature() { + // Pour le staff, utiliser l'organisation sélectionnée, sinon l'organisation de l'utilisateur + const orgIdToUse = (userInfo?.isStaff && selectedOrgId) ? selectedOrgId : userInfo?.orgId; + + console.log('🔍 [loadSignature] Debug:', { + isStaff: userInfo?.isStaff, + selectedOrgId, + userOrgId: userInfo?.orgId, + orgIdToUse + }); + + if (!orgIdToUse) { + console.log('⚠️ Aucune organisation sélectionnée pour charger la signature'); + setCurrentSignature(null); + return; + } + + console.log('🔍 Chargement signature pour org_id:', orgIdToUse); + + try { + const res = await fetch(`/api/organization/signature?org_id=${orgIdToUse}`, { cache: 'no-store' }); + console.log('📡 Response status:', res.status); + + if (res.ok) { + const data = await res.json(); + console.log('📦 Response data:', data); + + // Vérifier le format de la signature + if (data.signature_b64) { + console.log('🖼️ Format signature:', { + length: data.signature_b64.length, + startsWithData: data.signature_b64.startsWith('data:'), + first50chars: data.signature_b64.substring(0, 50) + }); + } + + setCurrentSignature(data.signature_b64 || null); + console.log('✅ Signature chargée:', data.signature_b64 ? `présente (${data.signature_b64.substring(0, 50)}...)` : 'absente'); + } else { + console.error('❌ Erreur HTTP:', res.status, await res.text()); + } + } catch (e) { + console.error('❌ Erreur chargement signature:', e); + } + } + + // Convertir une image (PNG ou JPEG) en base64 PNG + async function convertImageToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + // Créer une image temporaire + const img = new Image(); + const reader = new FileReader(); + + reader.onload = (e) => { + img.src = e.target?.result as string; + }; + + img.onload = () => { + try { + // Créer un canvas pour convertir en PNG + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Impossible de créer le contexte canvas')); + return; + } + + // Dessiner l'image sur le canvas + ctx.drawImage(img, 0, 0); + + // Convertir en PNG avec le préfixe data URL complet + const pngDataUrl = canvas.toDataURL('image/png'); + + console.log('✅ Image convertie en PNG:', { + originalType: file.type, + originalSize: file.size, + dataUrlLength: pngDataUrl.length, + dimensions: `${img.width}x${img.height}`, + hasPrefix: pngDataUrl.startsWith('data:image/png;base64,') + }); + + // Retourner le data URL complet (data:image/png;base64,...) + resolve(pngDataUrl); + } catch (err) { + reject(err); + } + }; + + img.onerror = () => { + reject(new Error('Impossible de charger l\'image')); + }; + + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + // Sauvegarder la signature + async function saveSignature(base64: string) { + // Pour le staff, utiliser l'organisation sélectionnée, sinon l'organisation de l'utilisateur + const orgIdToUse = (userInfo?.isStaff && selectedOrgId) ? selectedOrgId : userInfo?.orgId; + + if (!orgIdToUse) { + setUploadMessage({ type: 'error', text: 'Organisation non identifiée' }); + return; + } + + console.log('💾 Sauvegarde signature pour org_id:', orgIdToUse); + + setUploadingSignature(true); + setUploadMessage(null); + + try { + const res = await fetch('/api/organization/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + org_id: orgIdToUse, + signature_b64: base64 + }) + }); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || 'Erreur lors de la sauvegarde'); + } + + setCurrentSignature(base64); + console.log('✅ Signature sauvegardée avec succès'); + setUploadMessage({ type: 'success', text: 'Signature enregistrée avec succès !' }); + + // Fermer le modal après 2 secondes et recharger les contrats + setTimeout(async () => { + setShowSignatureModal(false); + setUploadMessage(null); + + // Recharger les contrats pour mettre à jour les badges de signature + console.log('🔄 Rechargement des contrats après sauvegarde signature...'); + setReloadingAfterSignatureChange(true); + await load(); + setReloadingAfterSignatureChange(false); + }, 2000); + } catch (e: any) { + console.error('Erreur sauvegarde signature:', e); + setUploadMessage({ type: 'error', text: e.message || 'Impossible de sauvegarder la signature' }); + } finally { + setUploadingSignature(false); + } + } + + // Supprimer la signature + async function deleteSignature() { + // Pour le staff, utiliser l'organisation sélectionnée, sinon l'organisation de l'utilisateur + const orgIdToUse = (userInfo?.isStaff && selectedOrgId) ? selectedOrgId : userInfo?.orgId; + + if (!orgIdToUse) { + setUploadMessage({ type: 'error', text: 'Organisation non identifiée' }); + return; + } + + console.log('🗑️ Suppression signature pour org_id:', orgIdToUse); + + setUploadingSignature(true); + setUploadMessage(null); + setShowDeleteConfirm(false); + + try { + const res = await fetch('/api/organization/signature', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + org_id: orgIdToUse + }) + }); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || 'Erreur lors de la suppression'); + } + + setCurrentSignature(null); + console.log('✅ Signature supprimée avec succès'); + setUploadMessage({ type: 'success', text: 'Signature supprimée avec succès !' }); + + // Fermer le modal après 2 secondes et recharger les contrats + setTimeout(async () => { + setShowSignatureModal(false); + setUploadMessage(null); + + // Recharger les contrats pour mettre à jour les badges de signature + console.log('🔄 Rechargement des contrats après suppression signature...'); + setReloadingAfterSignatureChange(true); + await load(); + setReloadingAfterSignatureChange(false); + }, 2000); + } catch (e: any) { + console.error('Erreur suppression signature:', e); + setUploadMessage({ type: 'error', text: e.message || 'Impossible de supprimer la signature' }); + } finally { + setUploadingSignature(false); + } + } + + // Gérer l'upload d'un fichier image + async function handleFileUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + + await processImageFile(file); + + // Réinitialiser l'input pour permettre de ré-uploader le même fichier + if (fileInputRef.current) fileInputRef.current.value = ''; + } + + // Traiter un fichier image (validation + conversion + sauvegarde) + async function processImageFile(file: File) { + // Vérifier le type de fichier + if (!file.type.match(/^image\/(png|jpeg|jpg)$/i)) { + setUploadMessage({ type: 'error', text: 'Seuls les fichiers PNG et JPG sont acceptés' }); + return; + } + + // Vérifier la taille (max 5MB) + if (file.size > 5 * 1024 * 1024) { + setUploadMessage({ type: 'error', text: 'Le fichier est trop volumineux (max 5MB)' }); + return; + } + + try { + setUploadingSignature(true); + setUploadMessage(null); + const base64 = await convertImageToBase64(file); + await saveSignature(base64); + } catch (e: any) { + console.error('Erreur conversion image:', e); + setUploadMessage({ type: 'error', text: 'Erreur lors de la conversion de l\'image' }); + setUploadingSignature(false); + } + } + + // Gérer le drag over + function handleDragOver(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + } + + // Gérer le drag leave + function handleDragLeave(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + } + + // Gérer le drop + async function handleDrop(e: React.DragEvent) { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = e.dataTransfer.files; + if (files.length > 0) { + await processImageFile(files[0]); + } + } + // Load current contracts to sign (server-side API fetches Airtable) async function load() { try { @@ -142,8 +436,17 @@ export default function SignaturesElectroniques() { } useEffect(() => { + console.log('🔄 [useEffect] Déclenchement avec:', { selectedOrgId, userOrgId: userInfo?.orgId }); load(); - }, [selectedOrgId]); // Recharger quand l'organisation change + loadSignature(); // Charger la signature au démarrage et quand l'organisation change + }, [selectedOrgId, userInfo?.orgId]); // Recharger quand l'organisation change (staff ou user) + + // Réinitialiser le message quand le modal s'ouvre + useEffect(() => { + if (showSignatureModal) { + setUploadMessage(null); + } + }, [showSignatureModal]); // useEffect pour injecter le composant DocuSeal uniquement quand embedSrc change useEffect(() => { @@ -151,8 +454,8 @@ export default function SignaturesElectroniques() { const container = docusealContainerRef.current; - // Récupérer la signature depuis sessionStorage - const signature = sessionStorage.getItem('docuseal_signature_b64'); + // Utiliser la signature depuis l'état React au lieu de sessionStorage + const signature = activeSignature; // Construire les attributs let dataSignatureAttr = ''; @@ -172,7 +475,7 @@ export default function SignaturesElectroniques() { data-allow-typed-signature="false" ${dataSignatureAttr}> `; - }, [embedSrc]); // Ne se ré-exécute que quand embedSrc change + }, [embedSrc, activeSignature]); // Re-render quand embedSrc ou activeSignature change // Ajouter des écouteurs pour recharger les données quand un modal se ferme useEffect(() => { @@ -229,11 +532,15 @@ export default function SignaturesElectroniques() { // 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); + console.log('✅ [SIGNATURES] Signature trouvée, stockage dans l\'état React'); + const normalizedSignature = normalizeSignatureFormat(f.signature_b64); + if (normalizedSignature) { + setActiveSignature(normalizedSignature); + console.log('✅ [SIGNATURES] Format normalisé:', normalizedSignature.substring(0, 50) + '...'); + } } else { console.log('⚠️ [SIGNATURES] Pas de signature dans les données'); - sessionStorage.removeItem('docuseal_signature_b64'); + setActiveSignature(null); } // 1) Si l'URL d'embed est déjà en base (signature_link) @@ -424,7 +731,7 @@ export default function SignaturesElectroniques() {
{stats.total}
-
À signer par l’employeur
+
À signer par l'employeur
{stats.ready}
@@ -433,6 +740,70 @@ export default function SignaturesElectroniques() {
+ {/* Card informative sur la signature pré-remplie - Visible uniquement si une organisation est identifiée */} + {((userInfo?.isStaff && selectedOrgId) || (!userInfo?.isStaff && userInfo?.orgId)) && ( +
+
+ {/* Icon */} +
+
+ +
+
+ + {/* Content */} +
+
+
+

+ Nouveau : Signature pré-remplie automatiquement +

+

+ La signature du signataire des contrats de travail, qui était déjà utilisée pour les AEM et les bordereaux Congés Spectacles, est maintenant pré-remplie automatiquement sur les contrats de travail. +

+ + {/* Statut de la signature */} +
+ {currentSignature ? ( + <> +
+ + Signature connue +
+ + + ) : ( + <> +
+ + Signature non connue +
+ + + )} +
+ +
+ + Cette signature sera automatiquement proposée lors de la signature électronique des contrats, mais reste modifiable à chaque signature. +
+
+
+
+
+
+ )} +
{loading ? (
Chargement…
@@ -626,6 +997,198 @@ export default function SignaturesElectroniques() { )}
+ + {/* Modal de gestion de la signature */} + {showSignatureModal && ( +
+
+ {/* Header */} +
+

+ {currentSignature ? 'Modifier la signature' : 'Ajouter une signature'} +

+ +
+ + {/* Body */} +
+ {/* Message de succès/erreur */} + {uploadMessage && ( +
+
+ {uploadMessage.type === 'success' ? ( + + ) : ( + + )} +

+ {uploadMessage.text} +

+
+
+ )} + + {/* Affichage de la signature actuelle */} + {currentSignature && ( +
+
+ + {!showDeleteConfirm ? ( + + ) : ( +
+ Confirmer ? + + +
+ )} +
+
+ Signature actuelle +
+
+ )} + + {/* Upload de nouvelle signature */} +
+ +
!uploadingSignature && fileInputRef.current?.click()} + > + + +

+ {isDragging ? 'Déposez l\'image ici' : 'Cliquez ou glissez-déposez une image'} +

+

+ PNG ou JPG • Max 5 MB +

+
+ + {uploadingSignature && ( +
+ + + + + Traitement en cours... +
+ )} +
+ + {/* Informations */} +
+
+ +
+

À propos de la signature

+
    +
  • • Elle sera automatiquement pré-remplie sur tous vos contrats
  • +
  • • Vous pouvez la modifier à tout moment
  • +
+
+
+
+
+ + {/* Footer */} +
+ +
+
+
+ )} + + {/* Overlay de rechargement après modification de signature */} + {reloadingAfterSignatureChange && ( +
+
+
+
+
+
+
+
+

+ Mise à jour en cours... +

+

+ Actualisation des contrats avec la nouvelle signature +

+
+
+
+
+ )} ); } diff --git a/app/api/organization/signature/route.ts b/app/api/organization/signature/route.ts new file mode 100644 index 0000000..59d5c49 --- /dev/null +++ b/app/api/organization/signature/route.ts @@ -0,0 +1,200 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; + +// GET: Récupérer la signature d'une organisation +export async function GET(req: NextRequest) { + try { + // Vérifier l'authentification + const supabaseAuth = createRouteHandlerClient({ cookies }); + const { data: { user } } = await supabaseAuth.auth.getUser(); + + if (!user) { + console.log('❌ [GET /api/organization/signature] Non authentifié'); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + console.log('✅ [GET /api/organization/signature] Utilisateur authentifié:', user.id); + + // Récupérer l'org_id depuis les paramètres + const { searchParams } = new URL(req.url); + const orgId = searchParams.get('org_id'); + + console.log('🔍 [GET /api/organization/signature] org_id:', orgId); + + if (!orgId) { + console.log('❌ [GET /api/organization/signature] org_id manquant'); + return NextResponse.json({ error: 'org_id manquant' }, { status: 400 }); + } + + // Créer un client avec service role pour lire organization_details + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { auth: { persistSession: false } } + ); + + // Récupérer la signature depuis organization_details + const { data, error } = await supabase + .from('organization_details') + .select('signature_b64') + .eq('org_id', orgId) + .maybeSingle(); + + console.log('📦 [GET /api/organization/signature] Query result:', { data, error }); + + if (error) { + console.error('❌ [GET /api/organization/signature] Erreur Supabase:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + const signature = data?.signature_b64 || null; + console.log('✅ [GET /api/organization/signature] Signature:', signature ? `présente (${signature.substring(0, 50)}...)` : 'absente'); + + return NextResponse.json({ + signature_b64: signature + }); + + } catch (e: any) { + console.error('❌ [GET /api/organization/signature] Exception:', e); + return NextResponse.json({ error: e.message }, { status: 500 }); + } +} + +// POST: Sauvegarder ou mettre à jour la signature d'une organisation +export async function POST(req: NextRequest) { + try { + // Vérifier l'authentification + const supabaseAuth = createRouteHandlerClient({ cookies }); + const { data: { user } } = await supabaseAuth.auth.getUser(); + + if (!user) { + console.log('❌ [POST /api/organization/signature] Non authentifié'); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + console.log('✅ [POST /api/organization/signature] Utilisateur authentifié:', user.id); + + const body = await req.json(); + const { org_id, signature_b64 } = body; + + console.log('💾 [POST /api/organization/signature] org_id:', org_id); + console.log('💾 [POST /api/organization/signature] signature_b64 length:', signature_b64?.length); + + if (!org_id) { + return NextResponse.json({ error: 'org_id manquant' }, { status: 400 }); + } + + if (!signature_b64 || typeof signature_b64 !== 'string') { + console.log('❌ [POST /api/organization/signature] signature_b64 invalide:', typeof signature_b64); + return NextResponse.json({ error: 'signature_b64 invalide' }, { status: 400 }); + } + + // Créer un client avec service role pour écrire dans organization_details + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { auth: { persistSession: false } } + ); + + console.log('📝 [POST /api/organization/signature] Tentative upsert...'); + + // Vérifier d'abord si l'enregistrement existe + const { data: existing, error: selectError } = await supabase + .from('organization_details') + .select('org_id') + .eq('org_id', org_id) + .maybeSingle(); + + if (selectError) { + console.error('❌ [POST /api/organization/signature] Erreur SELECT:', selectError); + } else { + console.log('📦 [POST /api/organization/signature] Enregistrement existant:', existing ? 'oui' : 'non'); + } + + // Upsert dans organization_details + const { error: upsertError } = await supabase + .from('organization_details') + .upsert({ + org_id: org_id, + signature_b64: signature_b64 + }, { + onConflict: 'org_id' + }); + + if (upsertError) { + console.error('❌ [POST /api/organization/signature] Erreur upsert Supabase:', upsertError); + console.error('❌ [POST /api/organization/signature] Détails erreur:', JSON.stringify(upsertError, null, 2)); + return NextResponse.json({ error: upsertError.message }, { status: 500 }); + } + + console.log('✅ [POST /api/organization/signature] Signature sauvegardée avec succès'); + + return NextResponse.json({ + success: true, + message: 'Signature enregistrée avec succès' + }); + + } catch (e: any) { + console.error('❌ [POST /api/organization/signature] Exception:', e); + return NextResponse.json({ error: e.message }, { status: 500 }); + } +} + +// DELETE: Supprimer la signature d'une organisation +export async function DELETE(req: NextRequest) { + try { + // Vérifier l'authentification + const supabaseAuth = createRouteHandlerClient({ cookies }); + const { data: { user } } = await supabaseAuth.auth.getUser(); + + if (!user) { + console.log('❌ [DELETE /api/organization/signature] Non authentifié'); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + console.log('✅ [DELETE /api/organization/signature] Utilisateur authentifié:', user.id); + + const body = await req.json(); + const { org_id } = body; + + console.log('🗑️ [DELETE /api/organization/signature] org_id:', org_id); + + if (!org_id) { + return NextResponse.json({ error: 'org_id manquant' }, { status: 400 }); + } + + // Créer un client avec service role pour écrire dans organization_details + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { auth: { persistSession: false } } + ); + + console.log('📝 [DELETE /api/organization/signature] Tentative suppression...'); + + // Mettre à jour en mettant signature_b64 à NULL + const { error: updateError } = await supabase + .from('organization_details') + .update({ signature_b64: null }) + .eq('org_id', org_id); + + if (updateError) { + console.error('❌ [DELETE /api/organization/signature] Erreur update Supabase:', updateError); + console.error('❌ [DELETE /api/organization/signature] Détails erreur:', JSON.stringify(updateError, null, 2)); + return NextResponse.json({ error: updateError.message }, { status: 500 }); + } + + console.log('✅ [DELETE /api/organization/signature] Signature supprimée avec succès'); + + return NextResponse.json({ + success: true, + message: 'Signature supprimée avec succès' + }); + + } catch (e: any) { + console.error('❌ [DELETE /api/organization/signature] Exception:', e); + return NextResponse.json({ error: e.message }, { status: 500 }); + } +}