Système signature intégrée
This commit is contained in:
parent
2257203831
commit
f460c1da5a
4 changed files with 944 additions and 31 deletions
162
SIGNATURE_STATE_REFACTORING.md
Normal file
162
SIGNATURE_STATE_REFACTORING.md
Normal file
|
|
@ -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<string | null>(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<string | null>(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)
|
||||||
|
|
@ -582,6 +582,7 @@ export default function ContratPage() {
|
||||||
// State pour la modale de signature DocuSeal
|
// State pour la modale de signature DocuSeal
|
||||||
const [embedSrc, setEmbedSrc] = useState<string>("");
|
const [embedSrc, setEmbedSrc] = useState<string>("");
|
||||||
const [modalTitle, setModalTitle] = useState<string>("");
|
const [modalTitle, setModalTitle] = useState<string>("");
|
||||||
|
const [signatureB64ForDocuSeal, setSignatureB64ForDocuSeal] = useState<string | null>(null); // Signature pour pré-remplissage
|
||||||
|
|
||||||
// State pour la modale de chargement
|
// State pour la modale de chargement
|
||||||
const [isLoadingSignature, setIsLoadingSignature] = useState<boolean>(false);
|
const [isLoadingSignature, setIsLoadingSignature] = useState<boolean>(false);
|
||||||
|
|
@ -770,29 +771,16 @@ export default function ContratPage() {
|
||||||
console.log('✅ [SIGNATURE] URL embed trouvée:', embed);
|
console.log('✅ [SIGNATURE] URL embed trouvée:', embed);
|
||||||
setEmbedSrc(embed);
|
setEmbedSrc(embed);
|
||||||
|
|
||||||
// Stocker la signature pour l'ajouter au composant
|
// Stocker la signature dans l'etat React pour l'ajouter au composant
|
||||||
console.log('🔍 [SIGNATURE] Tentative de stockage de la signature...');
|
console.log('🔍 [SIGNATURE] Stockage de la signature dans l\'etat React...');
|
||||||
console.log('🔍 [SIGNATURE] signatureB64 value:', signatureB64);
|
console.log('🔍 [SIGNATURE] signatureB64 value:', signatureB64);
|
||||||
|
|
||||||
if (signatureB64) {
|
if (signatureB64) {
|
||||||
console.log('📝 [SIGNATURE] Stockage de la signature dans sessionStorage');
|
console.log('✅ [SIGNATURE] Signature B64 disponible pour pre-remplissage');
|
||||||
console.log('📝 [SIGNATURE] Longueur de la signature:', signatureB64.length);
|
setSignatureB64ForDocuSeal(signatureB64);
|
||||||
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 {
|
} else {
|
||||||
console.log('⚠️ [SIGNATURE] Pas de signature à stocker, nettoyage sessionStorage');
|
console.log('⚠️ [SIGNATURE] Aucune signature B64 disponible');
|
||||||
sessionStorage.removeItem('docuseal_signature_b64');
|
setSignatureB64ForDocuSeal(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Masquer la modale de chargement
|
// 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] Génération du HTML docuseal-form');
|
||||||
console.log('🎨 [SIGNATURE RENDER] embedSrc:', embedSrc);
|
console.log('🎨 [SIGNATURE RENDER] embedSrc:', embedSrc);
|
||||||
|
|
||||||
// Récupérer la signature depuis sessionStorage
|
// Utiliser la signature depuis l'etat React au lieu de sessionStorage
|
||||||
const signatureB64 = typeof window !== 'undefined' ? sessionStorage.getItem('docuseal_signature_b64') : null;
|
const signatureB64 = signatureB64ForDocuSeal;
|
||||||
|
|
||||||
console.log('🎨 [SIGNATURE RENDER] Signature depuis sessionStorage:', {
|
console.log('🎨 [SIGNATURE RENDER] Signature depuis l\'etat React:', {
|
||||||
exists: !!signatureB64,
|
exists: !!signatureB64,
|
||||||
length: signatureB64?.length,
|
length: signatureB64?.length,
|
||||||
preview: signatureB64?.substring(0, 50)
|
preview: signatureB64?.substring(0, 50)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
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 Script from 'next/script';
|
||||||
import { usePageTitle } from '@/hooks/usePageTitle';
|
import { usePageTitle } from '@/hooks/usePageTitle';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
@ -85,6 +85,7 @@ export default function SignaturesElectroniques() {
|
||||||
|
|
||||||
const [contrats, setContrats] = useState<ContratWithSignatures[]>([]);
|
const [contrats, setContrats] = useState<ContratWithSignatures[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [reloadingAfterSignatureChange, setReloadingAfterSignatureChange] = useState(false);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isOnline, setIsOnline] = useState(true);
|
const [isOnline, setIsOnline] = useState(true);
|
||||||
|
|
@ -109,12 +110,305 @@ export default function SignaturesElectroniques() {
|
||||||
// Référence pour le conteneur DocuSeal
|
// Référence pour le conteneur DocuSeal
|
||||||
const docusealContainerRef = useRef<HTMLDivElement>(null);
|
const docusealContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// États pour la gestion de la signature
|
||||||
|
const [showSignatureModal, setShowSignatureModal] = useState(false);
|
||||||
|
const [currentSignature, setCurrentSignature] = useState<string | null>(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<string | null>(null); // Signature pour DocuSeal (remplace sessionStorage)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Charger les infos utilisateur et organisations
|
// Charger les infos utilisateur et organisations
|
||||||
const { data: userInfo } = useUserInfo();
|
const { data: userInfo } = useUserInfo();
|
||||||
const { data: organizations } = useOrganizations();
|
const { data: organizations } = useOrganizations();
|
||||||
|
|
||||||
// Suppression de pollActive et pollTimer car le polling a été retiré
|
// 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<string> {
|
||||||
|
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<HTMLInputElement>) {
|
||||||
|
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<HTMLDivElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer le drag leave
|
||||||
|
function handleDragLeave(e: React.DragEvent<HTMLDivElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer le drop
|
||||||
|
async function handleDrop(e: React.DragEvent<HTMLDivElement>) {
|
||||||
|
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)
|
// Load current contracts to sign (server-side API fetches Airtable)
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -142,8 +436,17 @@ export default function SignaturesElectroniques() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('🔄 [useEffect] Déclenchement avec:', { selectedOrgId, userOrgId: userInfo?.orgId });
|
||||||
load();
|
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 pour injecter le composant DocuSeal uniquement quand embedSrc change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -151,8 +454,8 @@ export default function SignaturesElectroniques() {
|
||||||
|
|
||||||
const container = docusealContainerRef.current;
|
const container = docusealContainerRef.current;
|
||||||
|
|
||||||
// Récupérer la signature depuis sessionStorage
|
// Utiliser la signature depuis l'état React au lieu de sessionStorage
|
||||||
const signature = sessionStorage.getItem('docuseal_signature_b64');
|
const signature = activeSignature;
|
||||||
|
|
||||||
// Construire les attributs
|
// Construire les attributs
|
||||||
let dataSignatureAttr = '';
|
let dataSignatureAttr = '';
|
||||||
|
|
@ -172,7 +475,7 @@ export default function SignaturesElectroniques() {
|
||||||
data-allow-typed-signature="false"
|
data-allow-typed-signature="false"
|
||||||
${dataSignatureAttr}>
|
${dataSignatureAttr}>
|
||||||
</docuseal-form>`;
|
</docuseal-form>`;
|
||||||
}, [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
|
// Ajouter des écouteurs pour recharger les données quand un modal se ferme
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -229,11 +532,15 @@ export default function SignaturesElectroniques() {
|
||||||
|
|
||||||
// Gérer la signature pré-remplie si disponible
|
// Gérer la signature pré-remplie si disponible
|
||||||
if (f.signature_b64) {
|
if (f.signature_b64) {
|
||||||
console.log('✅ [SIGNATURES] Signature trouvée, stockage dans sessionStorage');
|
console.log('✅ [SIGNATURES] Signature trouvée, stockage dans l\'état React');
|
||||||
sessionStorage.setItem('docuseal_signature_b64', f.signature_b64);
|
const normalizedSignature = normalizeSignatureFormat(f.signature_b64);
|
||||||
|
if (normalizedSignature) {
|
||||||
|
setActiveSignature(normalizedSignature);
|
||||||
|
console.log('✅ [SIGNATURES] Format normalisé:', normalizedSignature.substring(0, 50) + '...');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ [SIGNATURES] Pas de signature dans les données');
|
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)
|
// 1) Si l'URL d'embed est déjà en base (signature_link)
|
||||||
|
|
@ -424,7 +731,7 @@ export default function SignaturesElectroniques() {
|
||||||
<div className="text-xl font-bold">{stats.total}</div>
|
<div className="text-xl font-bold">{stats.total}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border p-3">
|
<div className="rounded-lg border p-3">
|
||||||
<div className="text-xs text-slate-500">À signer par l’employeur</div>
|
<div className="text-xs text-slate-500">À signer par l'employeur</div>
|
||||||
<div className="text-xl font-bold">{stats.ready}</div>
|
<div className="text-xl font-bold">{stats.ready}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border p-3">
|
<div className="rounded-lg border p-3">
|
||||||
|
|
@ -433,6 +740,70 @@ export default function SignaturesElectroniques() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Card informative sur la signature pré-remplie - Visible uniquement si une organisation est identifiée */}
|
||||||
|
{((userInfo?.isStaff && selectedOrgId) || (!userInfo?.isStaff && userInfo?.orgId)) && (
|
||||||
|
<div className="mt-6 rounded-xl border-2 border-blue-100 bg-gradient-to-br from-blue-50/50 to-indigo-50/30 p-6 shadow-sm">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
|
||||||
|
<Sparkles className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900 mb-2 flex items-center gap-2">
|
||||||
|
<span>Nouveau : Signature pré-remplie automatiquement</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-600 leading-relaxed mb-3">
|
||||||
|
La signature du signataire des contrats de travail, qui était déjà utilisée pour les <strong>AEM</strong> et les <strong>bordereaux Congés Spectacles</strong>, est maintenant <strong>pré-remplie automatiquement</strong> sur les contrats de travail.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Statut de la signature */}
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
{currentSignature ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-100 border border-emerald-200">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||||
|
<span className="text-sm font-medium text-emerald-700">Signature connue</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSignatureModal(true)}
|
||||||
|
className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Voir / modifier la signature
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-100 border border-amber-200">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-600" />
|
||||||
|
<span className="text-sm font-medium text-amber-700">Signature non connue</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSignatureModal(true)}
|
||||||
|
className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Ajouter une signature
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-2 text-xs text-slate-500">
|
||||||
|
<Lightbulb className="w-4 h-4 text-slate-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span>Cette signature sera automatiquement proposée lors de la signature électronique des contrats, mais reste modifiable à chaque signature.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-slate-500">Chargement…</div>
|
<div className="text-slate-500">Chargement…</div>
|
||||||
|
|
@ -626,6 +997,198 @@ export default function SignaturesElectroniques() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
{/* Modal de gestion de la signature */}
|
||||||
|
{showSignatureModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b bg-gradient-to-r from-blue-50 to-indigo-50">
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900">
|
||||||
|
{currentSignature ? 'Modifier la signature' : 'Ajouter une signature'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowSignatureModal(false);
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg hover:bg-white/60 transition-colors"
|
||||||
|
aria-label="Fermer"
|
||||||
|
>
|
||||||
|
<XCircle className="w-5 h-5 text-slate-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)]">
|
||||||
|
{/* Message de succès/erreur */}
|
||||||
|
{uploadMessage && (
|
||||||
|
<div className={`mb-6 p-4 rounded-lg border-2 ${
|
||||||
|
uploadMessage.type === 'success'
|
||||||
|
? 'bg-emerald-50 border-emerald-200'
|
||||||
|
: 'bg-red-50 border-red-200'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{uploadMessage.type === 'success' ? (
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-emerald-600 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<p className={`text-sm font-medium ${
|
||||||
|
uploadMessage.type === 'success' ? 'text-emerald-800' : 'text-red-800'
|
||||||
|
}`}>
|
||||||
|
{uploadMessage.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Affichage de la signature actuelle */}
|
||||||
|
{currentSignature && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="text-sm font-medium text-slate-700">
|
||||||
|
Signature actuelle :
|
||||||
|
</label>
|
||||||
|
{!showDeleteConfirm ? (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
disabled={uploadingSignature}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<XCircle className="w-4 h-4" />
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-slate-600">Confirmer ?</span>
|
||||||
|
<button
|
||||||
|
onClick={deleteSignature}
|
||||||
|
disabled={uploadingSignature}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Oui, supprimer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
|
disabled={uploadingSignature}
|
||||||
|
className="px-2 py-1 text-xs font-medium text-slate-600 hover:text-slate-800 hover:bg-slate-100 rounded transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border-2 border-slate-200 bg-slate-50 p-4 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={normalizeSignatureFormat(currentSignature) || ''}
|
||||||
|
alt="Signature actuelle"
|
||||||
|
className="max-h-32 max-w-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upload de nouvelle signature */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
{currentSignature ? 'Remplacer par une nouvelle signature :' : 'Charger une signature :'}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className={`border-2 border-dashed rounded-lg p-8 text-center transition-all cursor-pointer ${
|
||||||
|
isDragging
|
||||||
|
? 'border-blue-500 bg-blue-100/50 scale-[1.02]'
|
||||||
|
: 'border-slate-300 hover:border-blue-400 hover:bg-blue-50/30'
|
||||||
|
} ${uploadingSignature ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => !uploadingSignature && fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/jpg"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
disabled={uploadingSignature}
|
||||||
|
className="hidden"
|
||||||
|
id="signature-upload"
|
||||||
|
/>
|
||||||
|
<FileSignature className={`w-12 h-12 mx-auto mb-3 transition-colors ${
|
||||||
|
isDragging ? 'text-blue-500' : 'text-slate-400'
|
||||||
|
}`} />
|
||||||
|
<p className="text-sm font-medium text-slate-700 mb-1">
|
||||||
|
{isDragging ? 'Déposez l\'image ici' : 'Cliquez ou glissez-déposez une image'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
PNG ou JPG • Max 5 MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploadingSignature && (
|
||||||
|
<div className="mt-4 flex items-center justify-center gap-2 text-blue-600">
|
||||||
|
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium">Traitement en cours...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Informations */}
|
||||||
|
<div className="mt-6 p-4 rounded-lg bg-blue-50 border border-blue-100">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800">
|
||||||
|
<p className="font-medium mb-1">À propos de la signature</p>
|
||||||
|
<ul className="text-xs space-y-1 text-blue-700">
|
||||||
|
<li>• Elle sera automatiquement pré-remplie sur tous vos contrats</li>
|
||||||
|
<li>• Vous pouvez la modifier à tout moment</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t bg-slate-50">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowSignatureModal(false);
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-200 transition-colors"
|
||||||
|
disabled={uploadingSignature}
|
||||||
|
>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Overlay de rechargement après modification de signature */}
|
||||||
|
{reloadingAfterSignatureChange && (
|
||||||
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl px-8 py-6 max-w-sm mx-4">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-16 h-16 border-4 border-blue-200 rounded-full"></div>
|
||||||
|
<div className="absolute top-0 left-0 w-16 h-16 border-4 border-blue-600 rounded-full border-t-transparent animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900 mb-1">
|
||||||
|
Mise à jour en cours...
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
Actualisation des contrats avec la nouvelle signature
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
200
app/api/organization/signature/route.ts
Normal file
200
app/api/organization/signature/route.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue