✨ 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'
178 lines
6.5 KiB
TypeScript
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 });
|
|
}
|
|
}
|