diff --git a/MIGRATION_EMPLOYEE_DOCUSEAL_SLUGS.md b/MIGRATION_EMPLOYEE_DOCUSEAL_SLUGS.md new file mode 100644 index 0000000..00cdd24 --- /dev/null +++ b/MIGRATION_EMPLOYEE_DOCUSEAL_SLUGS.md @@ -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 diff --git a/app/api/admin/migrate-employee-slugs/route.ts b/app/api/admin/migrate-employee-slugs/route.ts new file mode 100644 index 0000000..987194d --- /dev/null +++ b/app/api/admin/migrate-employee-slugs/route.ts @@ -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 } + ); + } +} diff --git a/app/api/docuseal-signature/route.ts b/app/api/docuseal-signature/route.ts index df7e64c..b501346 100644 --- a/app/api/docuseal-signature/route.ts +++ b/app/api/docuseal-signature/route.ts @@ -302,12 +302,15 @@ export async function POST(request: NextRequest) { // Récupérer les informations pour le lien de signature const employerSubmission = submissionResponse.data.find((sub: any) => sub.role === 'Employeur'); + const employeeSubmission = submissionResponse.data.find((sub: any) => sub.role === 'Salarié'); 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) const signatureLink = `https://staging.paie.odentas.fr/odentas-sign?docuseal_id=${embedCode}`; console.log('🔗 [SIGNATURE] Lien généré:', signatureLink); + console.log('🔗 [SIGNATURE] Slug salarié:', employeeSlug); if (employerSignatureB64) { console.log('✅ [SIGNATURE] Signature B64 disponible pour pré-remplissage côté client'); } @@ -357,7 +360,8 @@ export async function POST(request: NextRequest) { contractId, templateId, docusealSubmissionId, - signatureLink + signatureLink, + employeeSlug }); const supabaseResult = await supabase @@ -365,6 +369,7 @@ export async function POST(request: NextRequest) { .update({ docuseal_template_id: templateId, docuseal_submission_id: docusealSubmissionId, + employee_docuseal_slug: employeeSlug, signature_status: 'En attente', signature_link: signatureLink, updated_at: new Date().toISOString() diff --git a/app/api/signature-salarie/check-status/route.ts b/app/api/signature-salarie/check-status/route.ts index 0197f12..e2146ba 100644 --- a/app/api/signature-salarie/check-status/route.ts +++ b/app/api/signature-salarie/check-status/route.ts @@ -15,82 +15,20 @@ export async function GET(request: NextRequest) { const docuseal_id = searchParams.get('docuseal_id'); if (!docuseal_id) { - console.error('❌ Paramètre manquant: docuseal_id'); + console.error('Paramètre manquant: docuseal_id'); return NextResponse.json( { error: 'Paramètre docuseal_id requis' }, { 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 const supabase = createSbServiceRole(); - // 1. Récupérer la submission depuis DocuSeal via le proxy - console.log('📞 Appel API DocuSeal pour récupérer les submissions'); - - 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); + // NOUVELLE APPROCHE: Chercher d'abord le contrat avec le slug dans employee_docuseal_slug + console.log('Recherche du contrat par employee_docuseal_slug:', docuseal_id); const { data: contract, error: contractError } = await supabase .from('cddu_contracts') @@ -110,13 +48,15 @@ export async function GET(request: NextRequest) { production_name, structure, profession, - role + role, + docuseal_submission_id, + employee_docuseal_slug `) - .eq('docuseal_submission_id', submissionId) + .eq('employee_docuseal_slug', docuseal_id) .maybeSingle(); 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( { error: 'Erreur lors de la recherche du contrat' }, { status: 500 } @@ -124,25 +64,68 @@ export async function GET(request: NextRequest) { } 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( - { error: 'Contrat introuvable' }, + { error: 'Document introuvable' }, { 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 .from('salaries') - .select('prenom, nom, adresse_mail') + .select('prenom, nom, adresse_mail, date_naissance') .eq('id', contract.employee_id) .maybeSingle(); 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 .from('organizations') .select('name') @@ -150,48 +133,46 @@ export async function GET(request: NextRequest) { .maybeSingle(); 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é - const submitters = submission.submitters || []; - const employeeSubmitter = submitters.find((s: any) => s.slug === docuseal_id); - + // 4. Vérifier si le salarié a déjà signé const isSigned = employeeSubmitter && employeeSubmitter.status === 'completed'; - console.log('📊 Statut signature salarié:', { + console.log('Statut signature salarié:', { status: employeeSubmitter?.status, completed_at: employeeSubmitter?.completed_at, 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; 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 { const { getS3SignedUrlIfExists } = await import('@/lib/aws-s3'); downloadUrl = await getS3SignedUrlIfExists(contract.contract_pdf_s3_key, 3600); if (downloadUrl) { - console.log('✅ URL pré-signée générée'); + console.log('URL pré-signée générée'); } else { - console.warn('⚠️ PDF non trouvé dans S3'); + console.warn('PDF non trouvé dans S3'); } } 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 td = String(contract.type_d_embauche || "").toLowerCase(); const isRG = td.includes("régime général") || td.includes("regime general") || td === "rg"; const regime = isRG ? "RG" : (isMulti ? "CDDU_MULTI" : "CDDU_MONO"); - // 8. Retourner les infos + // 7. Retourner les infos return NextResponse.json({ isSigned, + employee_birthdate: employee?.date_naissance || null, contract: { contract_number: contract.contract_number, regime: regime, @@ -208,7 +189,7 @@ export async function GET(request: NextRequest) { }); } 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( { error: 'Erreur serveur lors de la vérification', diff --git a/app/api/signature-salarie/verify-birthdate/route.ts b/app/api/signature-salarie/verify-birthdate/route.ts index 6d58749..6ded571 100644 --- a/app/api/signature-salarie/verify-birthdate/route.ts +++ b/app/api/signature-salarie/verify-birthdate/route.ts @@ -13,99 +13,42 @@ import { createSbServiceRole } from '@/lib/supabaseServer'; */ export async function POST(request: NextRequest) { console.log('=== API Vérification Date de Naissance Salarié ==='); + console.log('Request URL:', request.url); + console.log('Request method:', request.method); 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 if (!docuseal_id || !birthdate) { - console.error('❌ Paramètres manquants:', { docuseal_id, birthdate }); + console.error('Paramètres manquants:', { docuseal_id, birthdate }); return NextResponse.json( { error: 'Paramètres manquants', verified: false }, { 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 const supabase = createSbServiceRole(); - // 1. Appeler l'API DocuSeal via le proxy interne pour récupérer les submissions et trouver celle avec ce slug - console.log('📞 Appel API DocuSeal (via proxy interne) pour trouver la submission avec le 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); + // NOUVELLE APPROCHE: Chercher d'abord le contrat avec le slug dans employee_docuseal_slug + console.log('Recherche du contrat par employee_docuseal_slug:', docuseal_id); const { data: contract, error: contractError } = await supabase .from('cddu_contracts') - .select('id, employee_id, contract_number') - .eq('docuseal_submission_id', submissionId) + .select('id, employee_id, contract_number, employee_docuseal_slug') + .eq('employee_docuseal_slug', docuseal_id) .maybeSingle(); 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( { error: 'Erreur lors de la recherche du contrat', verified: false }, { status: 500 } @@ -113,20 +56,20 @@ export async function POST(request: NextRequest) { } 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( { error: 'Document introuvable', verified: false }, { status: 404 } ); } - console.log('📄 Contrat trouvé:', { + console.log('Contrat trouvé:', { id: contract.id, employee_id: contract.employee_id, 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 .from('salaries') .select('date_naissance, prenom, nom, adresse_mail') @@ -134,7 +77,7 @@ export async function POST(request: NextRequest) { .maybeSingle(); 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( { error: 'Erreur lors de la vérification', verified: false }, { status: 500 } @@ -142,7 +85,7 @@ export async function POST(request: NextRequest) { } 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( { error: 'Salarié introuvable', verified: false }, { status: 404 } @@ -150,7 +93,7 @@ export async function POST(request: NextRequest) { } 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) return NextResponse.json({ verified: true, @@ -158,31 +101,31 @@ export async function POST(request: NextRequest) { }); } - console.log('👤 Salarié trouvé:', { + console.log('Salarié trouvé:', { prenom: salarie.prenom, nom: salarie.nom, 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) const dbBirthdate = new Date(salarie.date_naissance).toISOString().split('T')[0]; const inputBirthdate = new Date(birthdate).toISOString().split('T')[0]; - console.log('📅 Comparaison dates:', { + console.log('Comparaison dates:', { db: dbBirthdate, input: inputBirthdate, match: 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({ verified: true, message: 'Date de naissance vérifiée' }); } else { - console.log('❌ Date de naissance incorrecte'); + console.log('Date de naissance incorrecte'); return NextResponse.json( { error: 'Date de naissance incorrecte', @@ -193,11 +136,15 @@ export async function POST(request: NextRequest) { } } 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( { error: 'Erreur serveur lors de la vérification', - verified: false + verified: false, + details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); diff --git a/app/signature-salarie/page.tsx b/app/signature-salarie/page.tsx index 3ae9582..24475ed 100644 --- a/app/signature-salarie/page.tsx +++ b/app/signature-salarie/page.tsx @@ -6,15 +6,21 @@ import SignatureSalarieContent from "./SignatureSalarieContent"; export default async function SignatureSalariePage() { const sb = createSbServer(); - // Récupérer le statut de maintenance - const { data: maintenanceStatus } = await sb - .from("maintenance_status") - .select("*") - .single(); + // Vérifier si on est en localhost + const isLocalhost = process.env.NODE_ENV === 'development' || + process.env.NEXT_PUBLIC_SITE_URL?.includes('localhost'); - // Si en maintenance, rediriger vers la page de maintenance - if (maintenanceStatus?.is_maintenance_mode) { - redirect("/maintenance"); + // Récupérer le statut de maintenance uniquement si pas en localhost + if (!isLocalhost) { + 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 ( diff --git a/scripts/migrate-employee-docuseal-slugs.ts b/scripts/migrate-employee-docuseal-slugs.ts new file mode 100644 index 0000000..06d47c6 --- /dev/null +++ b/scripts/migrate-employee-docuseal-slugs.ts @@ -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 { + 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); + }); diff --git a/supabase_add_employee_docuseal_slug.sql b/supabase_add_employee_docuseal_slug.sql new file mode 100644 index 0000000..67b3507 --- /dev/null +++ b/supabase_add_employee_docuseal_slug.sql @@ -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';