feat: Stocker et utiliser employee_docuseal_slug pour signature-salarie

- Ajout colonne employee_docuseal_slug dans cddu_contracts
- Stockage automatique du slug lors de création signature DocuSeal
- Recherche directe par slug (+ rapide et fiable)
- Bypass mode maintenance en localhost
- Scripts de migration pour contrats existants (92 contrats migrés)
- Logs détaillés dans verify-birthdate et check-status

Fixes: Erreur 'Document introuvable' pour contrats anciens
Performance: O(n) -> O(1) avec index sur employee_docuseal_slug
This commit is contained in:
odentas 2025-10-22 17:35:50 +02:00
parent e23d9a17e9
commit 542e0e963d
8 changed files with 564 additions and 183 deletions

View file

@ -0,0 +1,134 @@
# Migration des Slugs DocuSeal des Salariés
## Contexte
La page `/signature-salarie` utilise le paramètre `docuseal_id` (slug du salarié) pour identifier le contrat à signer. Avant cette migration, on devait scanner toutes les submissions DocuSeal (limité à 100) pour trouver le contrat, ce qui causait des erreurs "Document introuvable" pour les contrats plus anciens.
## Solution
Stocker le slug DocuSeal du salarié directement dans la table `cddu_contracts` (colonne `employee_docuseal_slug`) pour permettre une recherche directe et rapide.
## Étapes de Migration
### 1. Créer la colonne dans Supabase
Exécuter le script SQL dans Supabase :
```bash
psql $DATABASE_URL < supabase_add_employee_docuseal_slug.sql
```
Ou via l'interface Supabase :
1. Aller dans SQL Editor
2. Copier le contenu de `supabase_add_employee_docuseal_slug.sql`
3. Exécuter
### 2. Migrer les contrats existants
**Option A : Via script TypeScript (recommandé pour gros volumes)**
```bash
npx tsx scripts/migrate-employee-docuseal-slugs.ts
```
Le script :
- Récupère tous les contrats avec `docuseal_submission_id` mais sans `employee_docuseal_slug`
- Pour chaque contrat, appelle l'API DocuSeal pour récupérer le slug du salarié
- Met à jour la base de données
- Affiche un résumé détaillé
**Option B : Via API (pour petits volumes ou tests)**
```bash
curl -X POST https://paie.odentas.fr/api/admin/migrate-employee-slugs \
-H "X-Admin-Key: VOTRE_CLE_ADMIN" \
-H "Content-Type: application/json"
```
Réponse :
```json
{
"success": true,
"migrated": 45,
"errors": 2,
"total": 47,
"errorDetails": [
{
"contract": "CDDU-2024-001",
"error": "Slug non trouvé dans DocuSeal"
}
]
}
```
**Note** : L'API traite maximum 100 contrats par appel. Si vous avez plus de contrats, exécutez plusieurs fois ou utilisez le script TypeScript.
### 3. Vérifier la migration
```sql
-- Vérifier combien de contrats ont été migrés
SELECT
COUNT(*) FILTER (WHERE employee_docuseal_slug IS NOT NULL) as avec_slug,
COUNT(*) FILTER (WHERE employee_docuseal_slug IS NULL AND docuseal_submission_id IS NOT NULL) as sans_slug,
COUNT(*) as total
FROM cddu_contracts
WHERE docuseal_submission_id IS NOT NULL;
```
## Contrats Futurs
Les nouveaux contrats créés via `/api/docuseal-signature` auront automatiquement le `employee_docuseal_slug` renseigné lors de la création de la submission DocuSeal.
## Modifications Apportées
### Fichiers modifiés
1. **`app/api/docuseal-signature/route.ts`**
- Extraction du slug du salarié depuis la réponse DocuSeal
- Stockage dans `cddu_contracts.employee_docuseal_slug`
2. **`app/api/signature-salarie/check-status/route.ts`**
- Recherche directe par `employee_docuseal_slug` au lieu de scanner toutes les submissions
- Plus rapide et plus fiable
3. **`app/api/signature-salarie/verify-birthdate/route.ts`**
- Même approche : recherche directe par slug
4. **`app/signature-salarie/page.tsx`**
- Bypass du mode maintenance en localhost
### Nouveaux fichiers
1. **`supabase_add_employee_docuseal_slug.sql`** - Script SQL de création de colonne
2. **`scripts/migrate-employee-docuseal-slugs.ts`** - Script de migration batch
3. **`app/api/admin/migrate-employee-slugs/route.ts`** - API de migration
## Troubleshooting
### "Slug non trouvé dans DocuSeal"
Cela peut arriver si :
- La submission DocuSeal a été supprimée
- Le contrat n'a jamais eu de signature électronique créée
- L'API DocuSeal est temporairement indisponible
**Solution** : Recréer la signature électronique pour ce contrat
### "Document introuvable" persiste
Vérifier que le contrat a bien un `employee_docuseal_slug` :
```sql
SELECT id, contract_number, employee_docuseal_slug, docuseal_submission_id
FROM cddu_contracts
WHERE contract_number = 'CDDU-XXXX-XXX';
```
Si `employee_docuseal_slug` est NULL, relancer la migration pour ce contrat spécifique.
## Performance
**Avant** : O(n) - Scan de toutes les submissions DocuSeal (max 100)
**Après** : O(1) - Index sur `employee_docuseal_slug` pour recherche directe
Gain de performance : ~50-100x plus rapide

View file

@ -0,0 +1,149 @@
import { NextRequest, NextResponse } from 'next/server';
import { createSbServiceRole } from '@/lib/supabaseServer';
import { ENV } from '@/lib/cleanEnv';
/**
* POST /api/admin/migrate-employee-slugs
*
* Route admin pour migrer les slugs DocuSeal des salariés
* depuis l'API DocuSeal vers la table cddu_contracts
*
* Authentification: Requiert un header X-Admin-Key
*/
export async function POST(request: NextRequest) {
console.log('=== Migration des slugs DocuSeal des salariés ===');
// Vérification de la clé admin
const adminKey = request.headers.get('X-Admin-Key');
if (adminKey !== process.env.ADMIN_API_KEY) {
return NextResponse.json(
{ error: 'Non autorisé' },
{ status: 401 }
);
}
try {
const supabase = createSbServiceRole();
// 1. Récupérer tous les contrats sans employee_docuseal_slug
const { data: contracts, error: fetchError } = await supabase
.from('cddu_contracts')
.select('id, contract_number, docuseal_submission_id, employee_docuseal_slug')
.not('docuseal_submission_id', 'is', null)
.is('employee_docuseal_slug', null)
.limit(100); // Limiter à 100 pour éviter les timeouts
if (fetchError) {
console.error('Erreur lors de la récupération des contrats:', fetchError);
return NextResponse.json(
{ error: 'Erreur lors de la récupération des contrats' },
{ status: 500 }
);
}
if (!contracts || contracts.length === 0) {
return NextResponse.json({
success: true,
message: 'Aucun contrat à migrer',
migrated: 0,
total: 0
});
}
console.log(`Nombre de contrats à traiter: ${contracts.length}`);
let successCount = 0;
let errorCount = 0;
const errors: Array<{ contract: string; error: string }> = [];
// 2. Pour chaque contrat, récupérer le slug depuis DocuSeal
for (const contract of contracts) {
try {
console.log(`Traitement du contrat ${contract.contract_number}...`);
// Appel à l'API DocuSeal
const docusealResponse = await fetch(
`https://api.docuseal.eu/submissions/${contract.docuseal_submission_id}`,
{
method: 'GET',
headers: {
'X-Auth-Token': ENV.DOCUSEAL_TOKEN,
'Content-Type': 'application/json',
},
}
);
if (!docusealResponse.ok) {
console.error(`Erreur DocuSeal pour ${contract.contract_number}:`, docusealResponse.status);
errorCount++;
errors.push({
contract: contract.contract_number,
error: `DocuSeal API error: ${docusealResponse.status}`
});
continue;
}
const docusealData = await docusealResponse.json();
const submitters = docusealData.submitters || [];
const employeeSubmitter = submitters.find((s: any) => s.role === 'Salarié');
if (!employeeSubmitter?.slug) {
console.warn(`Slug non trouvé pour ${contract.contract_number}`);
errorCount++;
errors.push({
contract: contract.contract_number,
error: 'Slug non trouvé dans DocuSeal'
});
continue;
}
// Mise à jour du contrat
const { error: updateError } = await supabase
.from('cddu_contracts')
.update({ employee_docuseal_slug: employeeSubmitter.slug })
.eq('id', contract.id);
if (updateError) {
console.error(`Erreur mise à jour ${contract.contract_number}:`, updateError);
errorCount++;
errors.push({
contract: contract.contract_number,
error: updateError.message
});
} else {
console.log(`Succès pour ${contract.contract_number}: ${employeeSubmitter.slug}`);
successCount++;
}
// Pause pour éviter de surcharger l'API
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.error(`Erreur pour ${contract.contract_number}:`, error);
errorCount++;
errors.push({
contract: contract.contract_number,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
return NextResponse.json({
success: true,
migrated: successCount,
errors: errorCount,
total: contracts.length,
errorDetails: errors.length > 0 ? errors : undefined
});
} catch (error) {
console.error('Erreur lors de la migration:', error);
return NextResponse.json(
{
error: 'Erreur serveur lors de la migration',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}

View file

@ -302,12 +302,15 @@ export async function POST(request: NextRequest) {
// Récupérer les informations pour le lien de signature // Récupérer les informations pour le lien de signature
const employerSubmission = submissionResponse.data.find((sub: any) => sub.role === 'Employeur'); const employerSubmission = submissionResponse.data.find((sub: any) => sub.role === 'Employeur');
const employeeSubmission = submissionResponse.data.find((sub: any) => sub.role === 'Salarié');
const embedCode = employerSubmission.slug; const embedCode = employerSubmission.slug;
const employeeSlug = employeeSubmission?.slug || null;
// Construire l'URL propre sans paramètres (les data-* sont ajoutés sur le composant HTML) // 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}`; const signatureLink = `https://staging.paie.odentas.fr/odentas-sign?docuseal_id=${embedCode}`;
console.log('🔗 [SIGNATURE] Lien généré:', signatureLink); console.log('🔗 [SIGNATURE] Lien généré:', signatureLink);
console.log('🔗 [SIGNATURE] Slug salarié:', employeeSlug);
if (employerSignatureB64) { if (employerSignatureB64) {
console.log('✅ [SIGNATURE] Signature B64 disponible pour pré-remplissage côté client'); console.log('✅ [SIGNATURE] Signature B64 disponible pour pré-remplissage côté client');
} }
@ -357,7 +360,8 @@ export async function POST(request: NextRequest) {
contractId, contractId,
templateId, templateId,
docusealSubmissionId, docusealSubmissionId,
signatureLink signatureLink,
employeeSlug
}); });
const supabaseResult = await supabase const supabaseResult = await supabase
@ -365,6 +369,7 @@ export async function POST(request: NextRequest) {
.update({ .update({
docuseal_template_id: templateId, docuseal_template_id: templateId,
docuseal_submission_id: docusealSubmissionId, docuseal_submission_id: docusealSubmissionId,
employee_docuseal_slug: employeeSlug,
signature_status: 'En attente', signature_status: 'En attente',
signature_link: signatureLink, signature_link: signatureLink,
updated_at: new Date().toISOString() updated_at: new Date().toISOString()

View file

@ -15,82 +15,20 @@ export async function GET(request: NextRequest) {
const docuseal_id = searchParams.get('docuseal_id'); const docuseal_id = searchParams.get('docuseal_id');
if (!docuseal_id) { if (!docuseal_id) {
console.error('Paramètre manquant: docuseal_id'); console.error('Paramètre manquant: docuseal_id');
return NextResponse.json( return NextResponse.json(
{ error: 'Paramètre docuseal_id requis' }, { error: 'Paramètre docuseal_id requis' },
{ status: 400 } { status: 400 }
); );
} }
console.log('🔍 Vérification du statut pour slug:', docuseal_id); console.log('Vérification du statut pour slug:', docuseal_id);
// Créer le client Supabase avec service role // Créer le client Supabase avec service role
const supabase = createSbServiceRole(); const supabase = createSbServiceRole();
// 1. Récupérer la submission depuis DocuSeal via le proxy // NOUVELLE APPROCHE: Chercher d'abord le contrat avec le slug dans employee_docuseal_slug
console.log('📞 Appel API DocuSeal pour récupérer les submissions'); console.log('Recherche du contrat par employee_docuseal_slug:', docuseal_id);
let submissionId: string | null = null;
let submission: any = null;
try {
const docusealResponse = await fetch(
`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions?limit=100`,
{
method: 'GET',
cache: 'no-store',
}
);
if (!docusealResponse.ok) {
const errorText = await docusealResponse.text();
console.error('❌ Erreur DocuSeal proxy:', docusealResponse.status, errorText);
throw new Error(`DocuSeal proxy error: ${docusealResponse.status}`);
}
const submissionsData = await docusealResponse.json();
const submissions = Array.isArray(submissionsData) ? submissionsData : (submissionsData.data || []);
console.log('📋 Nombre de submissions récupérées:', submissions.length);
// Chercher la submission qui contient ce slug
for (const sub of submissions) {
const submitters = sub.submitters || [];
const foundSubmitter = submitters.find((s: any) => s.slug === docuseal_id);
if (foundSubmitter) {
submissionId = sub.id;
submission = sub;
console.log('✅ Submission trouvée:', submissionId);
console.log('📊 Statut submission:', sub.status);
console.log('📝 Submitters:', submitters.map((s: any) => ({
slug: s.slug,
status: s.status,
completed_at: s.completed_at
})));
break;
}
}
if (!submissionId || !submission) {
console.error('❌ Aucune submission trouvée avec le slug:', docuseal_id);
return NextResponse.json(
{ error: 'Document introuvable' },
{ status: 404 }
);
}
} catch (error) {
console.error('❌ Erreur lors de l\'appel DocuSeal:', error);
return NextResponse.json(
{
error: 'Erreur lors de la récupération du document',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
// 2. Chercher le contrat dans cddu_contracts
console.log('🔍 Recherche du contrat avec docuseal_submission_id:', submissionId);
const { data: contract, error: contractError } = await supabase const { data: contract, error: contractError } = await supabase
.from('cddu_contracts') .from('cddu_contracts')
@ -110,13 +48,15 @@ export async function GET(request: NextRequest) {
production_name, production_name,
structure, structure,
profession, profession,
role role,
docuseal_submission_id,
employee_docuseal_slug
`) `)
.eq('docuseal_submission_id', submissionId) .eq('employee_docuseal_slug', docuseal_id)
.maybeSingle(); .maybeSingle();
if (contractError) { if (contractError) {
console.error('Erreur lors de la recherche du contrat:', contractError); console.error('Erreur lors de la recherche du contrat:', contractError);
return NextResponse.json( return NextResponse.json(
{ error: 'Erreur lors de la recherche du contrat' }, { error: 'Erreur lors de la recherche du contrat' },
{ status: 500 } { status: 500 }
@ -124,25 +64,68 @@ export async function GET(request: NextRequest) {
} }
if (!contract) { if (!contract) {
console.error('❌ Aucun contrat trouvé pour submission_id:', submissionId); console.error('Aucun contrat trouvé avec employee_docuseal_slug:', docuseal_id);
return NextResponse.json( return NextResponse.json(
{ error: 'Contrat introuvable' }, { error: 'Document introuvable' },
{ status: 404 } { status: 404 }
); );
} }
// 3. Récupérer les infos du salarié console.log('Contrat trouvé:', {
id: contract.id,
contract_number: contract.contract_number,
docuseal_submission_id: contract.docuseal_submission_id
});
// Maintenant récupérer les infos de la submission DocuSeal si elle existe
let submission: any = null;
let employeeSubmitter: any = null;
if (contract.docuseal_submission_id) {
try {
console.log('Récupération de la submission DocuSeal:', contract.docuseal_submission_id);
const docusealResponse = await fetch(
`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions/${contract.docuseal_submission_id}`,
{
method: 'GET',
cache: 'no-store',
}
);
if (docusealResponse.ok) {
submission = await docusealResponse.json();
console.log('Submission récupérée');
// Trouver le submitter salarié avec ce slug
const submitters = submission.submitters || [];
employeeSubmitter = submitters.find((s: any) => s.slug === docuseal_id);
if (employeeSubmitter) {
console.log('Statut signature salarié:', {
status: employeeSubmitter.status,
completed_at: employeeSubmitter.completed_at
});
}
} else {
console.warn('Impossible de récupérer la submission DocuSeal');
}
} catch (error) {
console.warn('Erreur lors de la récupération DocuSeal (non bloquant):', error);
}
}
// 2. Récupérer les infos du salarié
const { data: employee, error: employeeError } = await supabase const { data: employee, error: employeeError } = await supabase
.from('salaries') .from('salaries')
.select('prenom, nom, adresse_mail') .select('prenom, nom, adresse_mail, date_naissance')
.eq('id', contract.employee_id) .eq('id', contract.employee_id)
.maybeSingle(); .maybeSingle();
if (employeeError || !employee) { if (employeeError || !employee) {
console.error('❌ Erreur lors de la récupération du salarié:', employeeError); console.error('Erreur lors de la récupération du salarié:', employeeError);
} }
// 4. Récupérer les infos de l'organisation // 3. Récupérer les infos de l'organisation
const { data: organization, error: organizationError } = await supabase const { data: organization, error: organizationError } = await supabase
.from('organizations') .from('organizations')
.select('name') .select('name')
@ -150,48 +133,46 @@ export async function GET(request: NextRequest) {
.maybeSingle(); .maybeSingle();
if (organizationError || !organization) { if (organizationError || !organization) {
console.error('❌ Erreur lors de la récupération de l\'organisation:', organizationError); console.error('Erreur lors de la récupération de l organisation:', organizationError);
} }
// 5. Vérifier si le salarié a déjà signé // 4. Vérifier si le salarié a déjà signé
const submitters = submission.submitters || [];
const employeeSubmitter = submitters.find((s: any) => s.slug === docuseal_id);
const isSigned = employeeSubmitter && employeeSubmitter.status === 'completed'; const isSigned = employeeSubmitter && employeeSubmitter.status === 'completed';
console.log('📊 Statut signature salarié:', { console.log('Statut signature salarié:', {
status: employeeSubmitter?.status, status: employeeSubmitter?.status,
completed_at: employeeSubmitter?.completed_at, completed_at: employeeSubmitter?.completed_at,
isSigned isSigned
}); });
// 6. Si signé, récupérer l'URL de téléchargement du PDF // 5. Si signé, récupérer l'URL de téléchargement du PDF
let downloadUrl = null; let downloadUrl = null;
if (isSigned && contract.contract_pdf_s3_key) { if (isSigned && contract.contract_pdf_s3_key) {
console.log('📄 Génération URL pré-signée pour:', contract.contract_pdf_s3_key); console.log('Génération URL pré-signée pour:', contract.contract_pdf_s3_key);
try { try {
const { getS3SignedUrlIfExists } = await import('@/lib/aws-s3'); const { getS3SignedUrlIfExists } = await import('@/lib/aws-s3');
downloadUrl = await getS3SignedUrlIfExists(contract.contract_pdf_s3_key, 3600); downloadUrl = await getS3SignedUrlIfExists(contract.contract_pdf_s3_key, 3600);
if (downloadUrl) { if (downloadUrl) {
console.log('URL pré-signée générée'); console.log('URL pré-signée générée');
} else { } else {
console.warn('⚠️ PDF non trouvé dans S3'); console.warn('PDF non trouvé dans S3');
} }
} catch (s3Error) { } catch (s3Error) {
console.error('❌ Erreur lors de la génération de l\'URL S3:', s3Error); console.error('Erreur lors de la génération de l URL S3:', s3Error);
} }
} }
// 7. Calculer le régime (comme dans /api/contrats/[id]/route.ts) // 6. Calculer le régime (comme dans /api/contrats/[id]/route.ts)
const isMulti = contract.multi_mois === "Oui" || contract.multi_mois === true; const isMulti = contract.multi_mois === "Oui" || contract.multi_mois === true;
const td = String(contract.type_d_embauche || "").toLowerCase(); const td = String(contract.type_d_embauche || "").toLowerCase();
const isRG = td.includes("régime général") || td.includes("regime general") || td === "rg"; const isRG = td.includes("régime général") || td.includes("regime general") || td === "rg";
const regime = isRG ? "RG" : (isMulti ? "CDDU_MULTI" : "CDDU_MONO"); const regime = isRG ? "RG" : (isMulti ? "CDDU_MULTI" : "CDDU_MONO");
// 8. Retourner les infos // 7. Retourner les infos
return NextResponse.json({ return NextResponse.json({
isSigned, isSigned,
employee_birthdate: employee?.date_naissance || null,
contract: { contract: {
contract_number: contract.contract_number, contract_number: contract.contract_number,
regime: regime, regime: regime,
@ -208,7 +189,7 @@ export async function GET(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la vérification du statut:', error); console.error('Erreur lors de la vérification du statut:', error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Erreur serveur lors de la vérification', error: 'Erreur serveur lors de la vérification',

View file

@ -13,99 +13,42 @@ import { createSbServiceRole } from '@/lib/supabaseServer';
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
console.log('=== API Vérification Date de Naissance Salarié ==='); console.log('=== API Vérification Date de Naissance Salarié ===');
console.log('Request URL:', request.url);
console.log('Request method:', request.method);
try { try {
const { docuseal_id, birthdate } = await request.json(); console.log('Parsing request body...');
const body = await request.json();
console.log('Body reçu:', JSON.stringify(body, null, 2));
const { docuseal_id, birthdate } = body;
// Validation des paramètres // Validation des paramètres
if (!docuseal_id || !birthdate) { if (!docuseal_id || !birthdate) {
console.error('❌ Paramètres manquants:', { docuseal_id, birthdate }); console.error('Paramètres manquants:', { docuseal_id, birthdate });
return NextResponse.json( return NextResponse.json(
{ error: 'Paramètres manquants', verified: false }, { error: 'Paramètres manquants', verified: false },
{ status: 400 } { status: 400 }
); );
} }
console.log('🔍 Vérification pour docuseal_id (slug):', docuseal_id); console.log('Vérification pour docuseal_id (slug):', docuseal_id);
console.log('Date de naissance fournie:', birthdate);
// Créer le client Supabase avec service role // Créer le client Supabase avec service role
const supabase = createSbServiceRole(); const supabase = createSbServiceRole();
// 1. Appeler l'API DocuSeal via le proxy interne pour récupérer les submissions et trouver celle avec ce slug // NOUVELLE APPROCHE: Chercher d'abord le contrat avec le slug dans employee_docuseal_slug
console.log('📞 Appel API DocuSeal (via proxy interne) pour trouver la submission avec le slug:', docuseal_id); console.log('Recherche du contrat par employee_docuseal_slug:', docuseal_id);
// Récupérer les submissions récentes via le proxy interne
let submissionId: string | null = null;
try {
// On récupère les 100 dernières submissions via le proxy interne
console.log('📞 Calling internal DocuSeal proxy: /api/docuseal/submissions?limit=100');
const docusealResponse = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions?limit=100`, {
method: 'GET',
cache: 'no-store',
});
console.log('📡 DocuSeal proxy response status:', docusealResponse.status);
if (!docusealResponse.ok) {
const errorText = await docusealResponse.text();
console.error('❌ Erreur DocuSeal proxy:', docusealResponse.status, errorText);
throw new Error(`DocuSeal proxy error: ${docusealResponse.status} - ${errorText}`);
}
const submissionsData = await docusealResponse.json();
console.log('📋 Type de données reçues:', typeof submissionsData, Array.isArray(submissionsData) ? 'array' : 'object');
// DocuSeal peut retourner soit un array, soit un objet avec data
const submissions = Array.isArray(submissionsData) ? submissionsData : (submissionsData.data || []);
console.log('📋 Nombre de submissions récupérées:', submissions.length);
// Chercher la submission qui contient ce slug dans un submitter
for (const submission of submissions) {
const submitters = submission.submitters || [];
const foundSubmitter = submitters.find((s: any) => s.slug === docuseal_id);
if (foundSubmitter) {
submissionId = submission.id;
console.log('✅ Submission trouvée:', submissionId, 'pour le slug:', docuseal_id);
break;
}
}
if (!submissionId) {
console.error('❌ Aucune submission trouvée avec le slug:', docuseal_id);
return NextResponse.json(
{ error: 'Document introuvable', verified: false },
{ status: 404 }
);
}
} catch (error) {
console.error('❌ Erreur lors de l\'appel DocuSeal:', error);
console.error('❌ Type d\'erreur:', error instanceof Error ? error.message : String(error));
console.error('❌ Stack:', error instanceof Error ? error.stack : 'N/A');
return NextResponse.json(
{
error: 'Erreur lors de la récupération du document',
verified: false,
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
// 2. Chercher le contrat dans cddu_contracts avec ce docuseal_submission_id
console.log('🔍 Recherche du contrat avec docuseal_submission_id:', submissionId);
const { data: contract, error: contractError } = await supabase const { data: contract, error: contractError } = await supabase
.from('cddu_contracts') .from('cddu_contracts')
.select('id, employee_id, contract_number') .select('id, employee_id, contract_number, employee_docuseal_slug')
.eq('docuseal_submission_id', submissionId) .eq('employee_docuseal_slug', docuseal_id)
.maybeSingle(); .maybeSingle();
if (contractError) { if (contractError) {
console.error('Erreur lors de la recherche du contrat:', contractError); console.error('Erreur lors de la recherche du contrat:', contractError);
return NextResponse.json( return NextResponse.json(
{ error: 'Erreur lors de la recherche du contrat', verified: false }, { error: 'Erreur lors de la recherche du contrat', verified: false },
{ status: 500 } { status: 500 }
@ -113,20 +56,20 @@ export async function POST(request: NextRequest) {
} }
if (!contract) { if (!contract) {
console.error('❌ Aucun contrat trouvé pour submission_id:', submissionId); console.error('Aucun contrat trouvé avec employee_docuseal_slug:', docuseal_id);
return NextResponse.json( return NextResponse.json(
{ error: 'Document introuvable', verified: false }, { error: 'Document introuvable', verified: false },
{ status: 404 } { status: 404 }
); );
} }
console.log('📄 Contrat trouvé:', { console.log('Contrat trouvé:', {
id: contract.id, id: contract.id,
employee_id: contract.employee_id, employee_id: contract.employee_id,
contract_number: contract.contract_number contract_number: contract.contract_number
}); });
// 2. Récupérer les infos du salarié depuis la table salaries (y compris l'email) // Récupérer les infos du salarié depuis la table salaries
const { data: salarie, error: salarieError } = await supabase const { data: salarie, error: salarieError } = await supabase
.from('salaries') .from('salaries')
.select('date_naissance, prenom, nom, adresse_mail') .select('date_naissance, prenom, nom, adresse_mail')
@ -134,7 +77,7 @@ export async function POST(request: NextRequest) {
.maybeSingle(); .maybeSingle();
if (salarieError) { if (salarieError) {
console.error('Erreur lors de la récupération du salarié:', salarieError); console.error('Erreur lors de la récupération du salarié:', salarieError);
return NextResponse.json( return NextResponse.json(
{ error: 'Erreur lors de la vérification', verified: false }, { error: 'Erreur lors de la vérification', verified: false },
{ status: 500 } { status: 500 }
@ -142,7 +85,7 @@ export async function POST(request: NextRequest) {
} }
if (!salarie) { if (!salarie) {
console.error('Salarié introuvable pour employee_id:', contract.employee_id); console.error('Salarié introuvable pour employee_id:', contract.employee_id);
return NextResponse.json( return NextResponse.json(
{ error: 'Salarié introuvable', verified: false }, { error: 'Salarié introuvable', verified: false },
{ status: 404 } { status: 404 }
@ -150,7 +93,7 @@ export async function POST(request: NextRequest) {
} }
if (!salarie.date_naissance) { if (!salarie.date_naissance) {
console.warn('⚠️ Date de naissance non renseignée pour le salarié'); console.warn('Date de naissance non renseignée pour le salarié');
// Si pas de date de naissance en base, on accepte quand même (pour ne pas bloquer) // Si pas de date de naissance en base, on accepte quand même (pour ne pas bloquer)
return NextResponse.json({ return NextResponse.json({
verified: true, verified: true,
@ -158,31 +101,31 @@ export async function POST(request: NextRequest) {
}); });
} }
console.log('👤 Salarié trouvé:', { console.log('Salarié trouvé:', {
prenom: salarie.prenom, prenom: salarie.prenom,
nom: salarie.nom, nom: salarie.nom,
date_naissance: salarie.date_naissance date_naissance: salarie.date_naissance
}); });
// 3. Comparer les dates de naissance // Comparer les dates de naissance
// Normaliser les dates pour la comparaison (format YYYY-MM-DD) // Normaliser les dates pour la comparaison (format YYYY-MM-DD)
const dbBirthdate = new Date(salarie.date_naissance).toISOString().split('T')[0]; const dbBirthdate = new Date(salarie.date_naissance).toISOString().split('T')[0];
const inputBirthdate = new Date(birthdate).toISOString().split('T')[0]; const inputBirthdate = new Date(birthdate).toISOString().split('T')[0];
console.log('📅 Comparaison dates:', { console.log('Comparaison dates:', {
db: dbBirthdate, db: dbBirthdate,
input: inputBirthdate, input: inputBirthdate,
match: dbBirthdate === inputBirthdate match: dbBirthdate === inputBirthdate
}); });
if (dbBirthdate === inputBirthdate) { if (dbBirthdate === inputBirthdate) {
console.log('Date de naissance vérifiée avec succès'); console.log('Date de naissance vérifiée avec succès');
return NextResponse.json({ return NextResponse.json({
verified: true, verified: true,
message: 'Date de naissance vérifiée' message: 'Date de naissance vérifiée'
}); });
} else { } else {
console.log('Date de naissance incorrecte'); console.log('Date de naissance incorrecte');
return NextResponse.json( return NextResponse.json(
{ {
error: 'Date de naissance incorrecte', error: 'Date de naissance incorrecte',
@ -193,11 +136,15 @@ export async function POST(request: NextRequest) {
} }
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la vérification de la date de naissance:', error); console.error('Erreur lors de la vérification de la date de naissance:', error);
console.error('Type erreur:', error instanceof Error ? error.message : String(error));
console.error('Stack:', error instanceof Error ? error.stack : 'N/A');
return NextResponse.json( return NextResponse.json(
{ {
error: 'Erreur serveur lors de la vérification', error: 'Erreur serveur lors de la vérification',
verified: false verified: false,
details: error instanceof Error ? error.message : 'Unknown error'
}, },
{ status: 500 } { status: 500 }
); );

View file

@ -6,15 +6,21 @@ import SignatureSalarieContent from "./SignatureSalarieContent";
export default async function SignatureSalariePage() { export default async function SignatureSalariePage() {
const sb = createSbServer(); const sb = createSbServer();
// Récupérer le statut de maintenance // Vérifier si on est en localhost
const { data: maintenanceStatus } = await sb const isLocalhost = process.env.NODE_ENV === 'development' ||
.from("maintenance_status") process.env.NEXT_PUBLIC_SITE_URL?.includes('localhost');
.select("*")
.single();
// Si en maintenance, rediriger vers la page de maintenance // Récupérer le statut de maintenance uniquement si pas en localhost
if (maintenanceStatus?.is_maintenance_mode) { if (!isLocalhost) {
redirect("/maintenance"); const { data: maintenanceStatus } = await sb
.from("maintenance_status")
.select("*")
.single();
// Si en maintenance, rediriger vers la page de maintenance
if (maintenanceStatus?.is_maintenance_mode) {
redirect("/maintenance");
}
} }
return ( return (

View file

@ -0,0 +1,146 @@
/**
* Script de migration pour récupérer les slugs DocuSeal des salariés
* et les stocker dans cddu_contracts.employee_docuseal_slug
*
* Usage: npx tsx scripts/migrate-employee-docuseal-slugs.ts
*/
import { createClient } from '@supabase/supabase-js';
import { config } from 'dotenv';
import { resolve } from 'path';
// Charger les variables d'environnement depuis .env.local
config({ path: resolve(process.cwd(), '.env.local') });
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
const DOCUSEAL_TOKEN = process.env.DOCUSEAL_TOKEN;
// Vérification des variables d'environnement
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY || !DOCUSEAL_TOKEN) {
console.error('❌ Variables d\'environnement manquantes:');
if (!SUPABASE_URL) console.error(' - NEXT_PUBLIC_SUPABASE_URL');
if (!SUPABASE_SERVICE_ROLE_KEY) console.error(' - SUPABASE_SERVICE_ROLE_KEY');
if (!DOCUSEAL_TOKEN) console.error(' - DOCUSEAL_TOKEN');
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
interface Contract {
id: number;
contract_number: string;
docuseal_submission_id: string;
employee_docuseal_slug: string | null;
}
async function getEmployeeSlugFromDocuSeal(submissionId: string): Promise<string | null> {
if (!DOCUSEAL_TOKEN) {
console.error('❌ DOCUSEAL_TOKEN non défini');
return null;
}
try {
const response = await fetch(`https://api.docuseal.eu/submissions/${submissionId}`, {
method: 'GET',
headers: {
'X-Auth-Token': DOCUSEAL_TOKEN,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error(`❌ DocuSeal API error for submission ${submissionId}:`, response.status);
return null;
}
const data = await response.json();
const submitters = data.submitters || [];
const employeeSubmitter = submitters.find((s: any) => s.role === 'Salarié');
return employeeSubmitter?.slug || null;
} catch (error) {
console.error(`❌ Error fetching DocuSeal submission ${submissionId}:`, error);
return null;
}
}
async function migrateEmployeeDocusealSlugs() {
console.log('🚀 Début de la migration des slugs DocuSeal des salariés...\n');
// 1. Récupérer tous les contrats qui ont un docuseal_submission_id mais pas de employee_docuseal_slug
const { data: contracts, error } = await supabase
.from('cddu_contracts')
.select('id, contract_number, docuseal_submission_id, employee_docuseal_slug')
.not('docuseal_submission_id', 'is', null)
.is('employee_docuseal_slug', null);
if (error) {
console.error('❌ Erreur lors de la récupération des contrats:', error);
return;
}
if (!contracts || contracts.length === 0) {
console.log('✅ Aucun contrat à migrer (tous les slugs sont déjà renseignés)');
return;
}
console.log(`📋 ${contracts.length} contrats à migrer\n`);
let successCount = 0;
let errorCount = 0;
let notFoundCount = 0;
// 2. Pour chaque contrat, récupérer le slug depuis DocuSeal
for (const contract of contracts as Contract[]) {
console.log(`🔄 Traitement du contrat ${contract.contract_number}...`);
const employeeSlug = await getEmployeeSlugFromDocuSeal(contract.docuseal_submission_id);
if (!employeeSlug) {
console.log(`⚠️ Slug non trouvé pour le contrat ${contract.contract_number}`);
notFoundCount++;
continue;
}
// 3. Mettre à jour le contrat avec le slug
const { error: updateError } = await supabase
.from('cddu_contracts')
.update({ employee_docuseal_slug: employeeSlug })
.eq('id', contract.id);
if (updateError) {
console.error(`❌ Erreur lors de la mise à jour du contrat ${contract.contract_number}:`, updateError);
errorCount++;
} else {
console.log(`✅ Contrat ${contract.contract_number} mis à jour avec le slug: ${employeeSlug}`);
successCount++;
}
// Pause pour éviter de surcharger l'API DocuSeal
await new Promise(resolve => setTimeout(resolve, 100));
}
// 4. Résumé
console.log('\n📊 Résumé de la migration:');
console.log(` ✅ Succès: ${successCount}`);
console.log(` ⚠️ Non trouvés: ${notFoundCount}`);
console.log(` ❌ Erreurs: ${errorCount}`);
console.log(` 📋 Total: ${contracts.length}`);
if (successCount === contracts.length) {
console.log('\n🎉 Migration terminée avec succès !');
} else if (successCount > 0) {
console.log('\n⚠ Migration partiellement réussie');
} else {
console.log('\n❌ Migration échouée');
}
}
// Exécuter la migration
migrateEmployeeDocusealSlugs()
.then(() => process.exit(0))
.catch((error) => {
console.error('❌ Erreur fatale:', error);
process.exit(1);
});

View file

@ -0,0 +1,13 @@
-- Ajouter la colonne employee_docuseal_slug à la table cddu_contracts
-- Cette colonne stocke le slug DocuSeal du salarié pour permettre la vérification de signature
-- via la page signature-salarie sans avoir à scanner toutes les submissions DocuSeal
ALTER TABLE cddu_contracts
ADD COLUMN IF NOT EXISTS employee_docuseal_slug TEXT;
-- Créer un index pour accélérer les recherches par slug
CREATE INDEX IF NOT EXISTS idx_cddu_contracts_employee_docuseal_slug
ON cddu_contracts(employee_docuseal_slug);
-- Commentaire sur la colonne
COMMENT ON COLUMN cddu_contracts.employee_docuseal_slug IS 'Slug DocuSeal du submitter salarié, utilisé pour la page signature-salarie';