espace-paie-odentas/app/api/signatures-electroniques/avenants/route.ts
odentas 5b72941777 feat: Système complet de gestion des avenants avec signatures électroniques
 Nouvelles fonctionnalités
- Page de gestion des avenants (/staff/avenants)
- Page de détail d'un avenant (/staff/avenants/[id])
- Création d'avenants (objet, durée, rémunération)
- Génération automatique de PDF d'avenant
- Signature électronique via DocuSeal (employeur puis salarié)
- Changement manuel du statut d'un avenant
- Suppression d'avenants

🔧 Routes API
- POST /api/staff/amendments/create - Créer un avenant
- POST /api/staff/amendments/generate-pdf - Générer le PDF
- POST /api/staff/amendments/[id]/send-signature - Envoyer en signature
- POST /api/staff/amendments/[id]/change-status - Changer le statut
- POST /api/webhooks/docuseal-amendment - Webhook après signature employeur
- GET /api/signatures-electroniques/avenants - Liste des avenants en signature

📧 Système email universel v2
- Migration vers le système universel v2 pour les emails d'avenants
- Template 'signature-request-employee-amendment' pour salariés
- Insertion automatique dans DynamoDB pour la Lambda
- Mise à jour automatique du statut dans Supabase

🗄️ Base de données
- Table 'avenants' avec tous les champs (objet, durée, rémunération)
- Colonnes de notification (last_employer_notification_at, last_employee_notification_at)
- Liaison avec cddu_contracts

🎨 Composants
- AvenantDetailPageClient - Détail complet d'un avenant
- ChangeStatusModal - Changement de statut manuel
- SendSignatureModal - Envoi en signature
- DeleteAvenantModal - Suppression avec confirmation
- AvenantSuccessModal - Confirmation de création

📚 Documentation
- AVENANT_EMAIL_SYSTEM_MIGRATION.md - Guide complet de migration

🐛 Corrections
- Fix parsing défensif dans Lambda AWS
- Fix récupération des données depuis DynamoDB
- Fix statut MFA !== 'verified' au lieu de === 'unverified'
2025-10-23 15:30:11 +02:00

178 lines
6.5 KiB
TypeScript

import { NextResponse, NextRequest } from 'next/server';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { detectDemoModeFromHeaders } from '@/lib/demo-detector';
// GET /api/signatures-electroniques/avenants
// Retourne les avenants à signer par l'employeur ou salarié depuis Supabase
export async function GET(req: NextRequest) {
// 🎭 Vérifier le mode démo - retourner des données vides pour sécuriser
if (detectDemoModeFromHeaders(req.headers)) {
console.log('🎭 [SIGNATURES AVENANTS API] Mode démo détecté - retour de données vides');
return NextResponse.json({ records: [] }, { status: 200 });
}
const reqUrl = new URL(req.url);
const scope = (reqUrl.searchParams.get('scope') || 'employeur').toLowerCase();
const orgIdParam = reqUrl.searchParams.get('org_id'); // Paramètre org_id pour les staff
// Récupération de l'organisation active
const sb = createRouteHandlerClient({ cookies });
const { data: { user } } = await sb.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
// Vérifier staff pour lire la cible via cookie active_org_id ou paramètre org_id
let isStaff = false;
try {
const { data } = await sb.from('staff_users').select('is_staff').eq('user_id', user.id).maybeSingle();
isStaff = !!data?.is_staff;
} catch {}
let orgId: string | null = null;
try {
if (isStaff) {
// Si un org_id est fourni en paramètre, l'utiliser (priorité pour les staff)
if (orgIdParam) {
orgId = orgIdParam;
} else {
// Sinon utiliser le cookie active_org_id
const c = cookies();
orgId = c.get('active_org_id')?.value || null;
}
} else {
const { data, error } = await sb
.from('organization_members')
.select('org_id')
.eq('user_id', user.id)
.single();
if (error || !data?.org_id) {
return NextResponse.json({ error: 'no_active_org' }, { status: 403 });
}
orgId = data.org_id;
}
} catch {}
// Si pas d'organisation trouvée :
// - pour les utilisateurs normaux on retourne une erreur
// - pour le staff on permet l'accès global
if (!orgId && !isStaff) {
return NextResponse.json({ error: 'no_active_org' }, { status: 403 });
}
// Construction de la requête Supabase
try {
// D'abord récupérer les avenants sans filtrer par org_id
let query = sb
.from('avenants')
.select(`
id,
contract_id,
type_avenant,
statut,
signature_status,
docuseal_submission_id,
pdf_url,
created_at
`);
// Filtrer selon le scope demandé
if (scope === 'salarie') {
// Avenants en attente de signature du salarié
query = query.eq('signature_status', 'pending_employee');
} else {
// Avenants en attente de signature de l'employeur (scope = 'employeur')
query = query.eq('signature_status', 'pending_employer');
}
// Trier par date de création décroissante
query = query.order('created_at', { ascending: false });
const { data, error } = await query;
if (error) {
console.error('❌ [signatures-avenants] Erreur Supabase:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
console.log(`🔍 [signatures-avenants] Requête pour scope: ${scope}, orgId: ${orgId || 'null'}`);
console.log(`📊 [signatures-avenants] Avenants trouvés (avant filtrage org): ${data?.length || 0}`);
// Récupérer les données des contrats séparément (avec org_id)
const contractIds = data?.map((a: any) => a.contract_id).filter(Boolean) || [];
console.log(`🔍 [signatures-avenants] Contract IDs à récupérer: ${contractIds.length}`);
let contractsData: any = {};
if (contractIds.length > 0) {
let contractQuery = sb
.from('cddu_contracts')
.select('id, reference, employee_name, employee_matricule, production_name, org_id')
.in('id', contractIds);
// Filtrer par org_id si nécessaire
if (orgId) {
contractQuery = contractQuery.eq('org_id', orgId);
}
const { data: contracts, error: contractsError } = await contractQuery;
if (contractsError) {
console.error('❌ [signatures-avenants] Erreur récupération contrats:', contractsError);
} else {
console.log(`✅ [signatures-avenants] Contrats récupérés: ${contracts?.length || 0}`);
if (contracts) {
contractsData = contracts.reduce((acc: any, c: any) => {
acc[c.id] = c;
return acc;
}, {});
}
}
}
if (data && data.length > 0) {
console.log('📋 [signatures-avenants] Échantillon:', data.slice(0, 3).map((a: any) => ({
id: a.id,
statut: a.statut,
signature_status: a.signature_status,
type_avenant: a.type_avenant,
contract_id: a.contract_id
})));
}
// Transformer les données au format compatible avec la page
// Filtrer uniquement les avenants dont le contrat a été trouvé (et correspond à l'org si filtrage)
const records = (data || [])
.filter((avenant: any) => contractsData[avenant.contract_id]) // Ne garder que les avenants avec contrat valide
.map((avenant: any) => {
const contract = contractsData[avenant.contract_id];
return {
id: avenant.id,
fields: {
id: avenant.id,
reference: contract.reference || 'N/A',
employee_name: contract.employee_name || 'N/A',
employee_matricule: contract.employee_matricule || '',
production_name: contract.production_name || '',
type_avenant: avenant.type_avenant,
statut: avenant.statut,
signature_status: avenant.signature_status,
docuseal_submission_id: avenant.docuseal_submission_id,
pdf_url: avenant.pdf_url,
created_at: avenant.created_at,
// Indicateur pour distinguer les avenants des contrats dans l'UI
is_avenant: true,
}
};
});
console.log(`📊 [signatures-avenants] Avenants après filtrage org: ${records.length}`);
return NextResponse.json({ records }, { status: 200 });
} catch (error: any) {
console.error('Erreur récupération avenants signatures:', error);
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
}
}