feat: Page signature-salarie adaptée pour contrats et avenants

- check-status: cherche dans cddu_contracts ET avenants
- verify-birthdate: cherche dans les deux tables
- Retourne documentType ('contrat' ou 'avenant')
- UI adaptée: AlreadySignedCard et ToSignCard affichent le type
- Titre dynamique: 'Contrat' ou 'Avenant' selon le document
- Textes adaptés: 'Voir et signer votre avenant/contrat'
This commit is contained in:
odentas 2025-10-23 15:59:45 +02:00
parent 3af6db788b
commit 58e8fa4584
6 changed files with 209 additions and 82 deletions

View file

@ -4,8 +4,8 @@ import { createSbServiceRole } from '@/lib/supabaseServer';
/**
* GET /api/signature-salarie/check-status?docuseal_id=xxx
*
* Vérifie si un contrat est déjà signé par le salarié
* Retourne les infos du contrat et l'URL de téléchargement si signé
* Vérifie si un contrat OU un avenant est déjà signé par le salarié
* Retourne les infos du document et l'URL de téléchargement si signé
*/
export async function GET(request: NextRequest) {
console.log('=== API Check Signature Status ===');
@ -27,9 +27,8 @@ export async function GET(request: NextRequest) {
// Créer le client Supabase avec service role
const supabase = createSbServiceRole();
// 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);
// ÉTAPE 1: Chercher d'abord dans les CONTRATS
console.log('🔍 Recherche dans cddu_contracts...');
const { data: contract, error: contractError } = await supabase
.from('cddu_contracts')
.select(`
@ -55,37 +54,121 @@ export async function GET(request: NextRequest) {
.eq('employee_docuseal_slug', docuseal_id)
.maybeSingle();
if (contractError) {
console.error('Erreur lors de la recherche du contrat:', contractError);
return NextResponse.json(
{ error: 'Erreur lors de la recherche du contrat' },
{ status: 500 }
);
// ÉTAPE 2: Si pas trouvé dans contrats, chercher dans les AVENANTS
let avenant = null;
if (!contract) {
console.log('🔍 Pas trouvé dans contrats, recherche dans avenants...');
const { data: avenantData, error: avenantError } = await supabase
.from('avenants')
.select(`
*,
cddu_contracts (
id,
contract_number,
employee_id,
org_id,
start_date,
end_date,
net,
gross_pay,
production_name,
structure,
profession,
role,
type_d_embauche,
multi_mois
)
`)
.eq('employee_docuseal_slug', docuseal_id)
.maybeSingle();
if (avenantError) {
console.error('❌ Erreur recherche avenant:', avenantError);
} else if (avenantData) {
console.log('✅ Avenant trouvé:', avenantData.numero_avenant);
avenant = avenantData;
}
}
if (!contract) {
console.error('Aucun contrat trouvé avec employee_docuseal_slug:', docuseal_id);
// ÉTAPE 3: Si rien trouvé dans les deux tables
if (!contract && !avenant) {
console.error('❌ Aucun document trouvé avec employee_docuseal_slug:', docuseal_id);
return NextResponse.json(
{ error: 'Document introuvable' },
{ status: 404 }
);
}
console.log('Contrat trouvé:', {
id: contract.id,
contract_number: contract.contract_number,
docuseal_submission_id: contract.docuseal_submission_id
// ÉTAPE 4: Déterminer le type de document et préparer les données
const isAvenant = !!avenant;
const documentType = isAvenant ? 'avenant' : 'contrat';
console.log(`📄 Type de document: ${documentType}`);
let documentData: any;
let employeeId: string;
let orgId: string;
let docusealSubmissionId: string | null;
let pdfS3Key: string | null;
let documentId: string;
if (isAvenant && avenant) {
const contractData = avenant.cddu_contracts;
documentData = {
numero: avenant.numero_avenant,
contract_number: contractData.contract_number,
start_date: contractData.start_date,
end_date: contractData.end_date,
gross_amount: contractData.gross_pay || contractData.net || 0,
production_name: contractData.production_name || contractData.structure,
profession: contractData.profession || contractData.role,
type_d_embauche: contractData.type_d_embauche,
multi_mois: contractData.multi_mois,
};
employeeId = contractData.employee_id;
orgId = contractData.org_id;
docusealSubmissionId = avenant.docuseal_submission_id;
pdfS3Key = avenant.pdf_s3_key;
documentId = avenant.id;
} else if (contract) {
documentData = {
numero: contract.contract_number,
contract_number: contract.contract_number,
start_date: contract.start_date,
end_date: contract.end_date,
gross_amount: contract.gross_pay || contract.net || 0,
production_name: contract.production_name || contract.structure,
profession: contract.profession || contract.role,
type_d_embauche: contract.type_de_contrat,
multi_mois: contract.multi_mois,
};
employeeId = contract.employee_id;
orgId = contract.org_id;
docusealSubmissionId = contract.docuseal_submission_id;
pdfS3Key = contract.contract_pdf_s3_key;
documentId = contract.id;
} else {
// Ne devrait jamais arriver ici car on a déjà vérifié plus haut
return NextResponse.json(
{ error: 'Document introuvable' },
{ status: 404 }
);
}
console.log('📋 Document:', {
type: documentType,
numero: documentData.numero,
docuseal_submission_id: docusealSubmissionId
});
// Maintenant récupérer les infos de la submission DocuSeal si elle existe
// ÉTAPE 5: 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) {
if (docusealSubmissionId) {
try {
console.log('Récupération de la submission DocuSeal:', contract.docuseal_submission_id);
console.log('🔍 Récupération de la submission DocuSeal:', docusealSubmissionId);
const docusealResponse = await fetch(
`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions/${contract.docuseal_submission_id}`,
`${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/docuseal/submissions/${docusealSubmissionId}`,
{
method: 'GET',
cache: 'no-store',
@ -94,95 +177,96 @@ export async function GET(request: NextRequest) {
if (docusealResponse.ok) {
submission = await docusealResponse.json();
console.log('Submission récupérée');
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é:', {
console.log('📝 Statut signature salarié:', {
status: employeeSubmitter.status,
completed_at: employeeSubmitter.completed_at
});
}
} else {
console.warn('Impossible de récupérer la submission DocuSeal');
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);
console.warn('⚠️ Erreur lors de la récupération DocuSeal (non bloquant):', error);
}
}
// 2. Récupérer les infos du salarié
// ÉTAPE 6: Récupérer les infos du salarié
const { data: employee, error: employeeError } = await supabase
.from('salaries')
.select('prenom, nom, adresse_mail, date_naissance')
.eq('id', contract.employee_id)
.eq('id', employeeId)
.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);
}
// 3. Récupérer les infos de l'organisation
// ÉTAPE 7: Récupérer les infos de l'organisation
const { data: organization, error: organizationError } = await supabase
.from('organizations')
.select('name')
.eq('id', contract.org_id)
.eq('id', orgId)
.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);
}
// 4. Vérifier si le salarié a déjà signé
// ÉTAPE 8: 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
});
// 5. Si signé, récupérer l'URL de téléchargement du PDF
// ÉTAPE 9: 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);
if (isSigned && pdfS3Key) {
console.log('🔗 Génération URL pré-signée pour:', pdfS3Key);
try {
const { getS3SignedUrlIfExists } = await import('@/lib/aws-s3');
downloadUrl = await getS3SignedUrlIfExists(contract.contract_pdf_s3_key, 3600);
downloadUrl = await getS3SignedUrlIfExists(pdfS3Key, 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);
}
}
// 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();
// ÉTAPE 10: Calculer le régime (comme dans /api/contrats/[id]/route.ts)
const isMulti = documentData.multi_mois === "Oui" || documentData.multi_mois === true;
const td = String(documentData.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");
// 7. Retourner les infos
// ÉTAPE 11: Retourner les infos
return NextResponse.json({
isSigned,
documentType, // Nouveau: type de document (contrat ou avenant)
employee_birthdate: employee?.date_naissance || null,
contract: {
contract_number: contract.contract_number,
contract_number: documentData.contract_number,
regime: regime,
start_date: contract.start_date,
end_date: contract.end_date,
gross_amount: contract.gross_pay || contract.net || 0,
start_date: documentData.start_date,
end_date: documentData.end_date,
gross_amount: documentData.gross_amount,
employee_name: employee ? `${employee.prenom} ${employee.nom}` : null,
organization_name: organization?.name || null,
production_name: contract.production_name || contract.structure || null,
profession: contract.profession || contract.role || null,
production_name: documentData.production_name,
profession: documentData.profession,
},
signed_at: employeeSubmitter?.completed_at || null,
downloadUrl: isSigned ? downloadUrl : null,

View file

@ -38,8 +38,8 @@ export async function POST(request: NextRequest) {
// Créer le client Supabase avec service role
const supabase = createSbServiceRole();
// 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);
// ÉTAPE 1: Chercher d'abord dans les CONTRATS
console.log('🔍 Recherche dans cddu_contracts...');
const { data: contract, error: contractError } = await supabase
.from('cddu_contracts')
@ -47,33 +47,66 @@ export async function POST(request: NextRequest) {
.eq('employee_docuseal_slug', docuseal_id)
.maybeSingle();
if (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 }
);
// ÉTAPE 2: Si pas trouvé dans contrats, chercher dans les AVENANTS
let avenant = null;
let employeeId = null;
if (contract) {
console.log('✅ Contrat trouvé:', {
id: contract.id,
employee_id: contract.employee_id,
contract_number: contract.contract_number
});
employeeId = contract.employee_id;
} else {
console.log('🔍 Pas trouvé dans contrats, recherche dans avenants...');
const { data: avenantData, error: avenantError } = await supabase
.from('avenants')
.select(`
id,
numero_avenant,
employee_docuseal_slug,
cddu_contracts!inner (
employee_id,
contract_number
)
`)
.eq('employee_docuseal_slug', docuseal_id)
.maybeSingle();
if (avenantError) {
console.error('❌ Erreur recherche avenant:', avenantError);
} else if (avenantData) {
console.log('✅ Avenant trouvé:', avenantData.numero_avenant);
avenant = avenantData;
// cddu_contracts devient un objet unique avec !inner
const contractData = avenantData.cddu_contracts as any;
employeeId = contractData?.employee_id;
}
}
if (!contract) {
console.error('Aucun contrat trouvé avec employee_docuseal_slug:', docuseal_id);
// ÉTAPE 3: Si rien trouvé dans les deux tables
if (!contract && !avenant) {
console.error('❌ Aucun document trouvé avec employee_docuseal_slug:', docuseal_id);
return NextResponse.json(
{ error: 'Document introuvable', verified: false },
{ status: 404 }
);
}
console.log('Contrat trouvé:', {
id: contract.id,
employee_id: contract.employee_id,
contract_number: contract.contract_number
});
if (!employeeId) {
console.error('❌ employee_id non trouvé');
return NextResponse.json(
{ error: 'Erreur de récupération du salarié', verified: false },
{ status: 500 }
);
}
// 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')
.eq('id', contract.employee_id)
.eq('id', employeeId)
.maybeSingle();
if (salarieError) {
@ -85,7 +118,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:', employeeId);
return NextResponse.json(
{ error: 'Salarié introuvable', verified: false },
{ status: 404 }

View file

@ -110,7 +110,7 @@ export async function POST(request: NextRequest) {
}
// 3. Préparer les données pour l'email
const signatureLink = `https://paie.odentas.fr/odentas-sign?docuseal_id=${employeeSlug}`;
const signatureLink = `https://paie.odentas.fr/signature-salarie/?docuseal_id=${employeeSlug}`;
// Formater la date de manière sécurisée
const formatDate = (dateStr?: string) => {

View file

@ -16,9 +16,11 @@ interface AlreadySignedCardProps {
};
signed_at: string | null;
downloadUrl: string | null;
documentType?: 'contrat' | 'avenant'; // Nouveau: type de document
}
export default function AlreadySignedCard({ contract, signed_at, downloadUrl }: AlreadySignedCardProps) {
export default function AlreadySignedCard({ contract, signed_at, downloadUrl, documentType = 'contrat' }: AlreadySignedCardProps) {
const documentLabel = documentType === 'avenant' ? 'Avenant' : 'Contrat';
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
@ -85,9 +87,9 @@ export default function AlreadySignedCard({ contract, signed_at, downloadUrl }:
<CheckCircle2 className="w-8 h-8" />
</div>
<div>
<h1 className="text-2xl font-bold">Contrat signé</h1>
<h1 className="text-2xl font-bold">{documentLabel} signé</h1>
<p className="text-green-50 text-sm mt-1">
Vous avez signé électroniquement ce contrat.
Vous avez signé électroniquement ce {documentLabel.toLowerCase()}.
</p>
</div>
</div>

View file

@ -24,12 +24,14 @@ export default function SignatureSalarieContent() {
contract: any;
signed_at: string | null;
downloadUrl: string | null;
documentType?: 'contrat' | 'avenant'; // Nouveau: type de document
} | null>(null);
const [showSignatureModal, setShowSignatureModal] = useState(false);
const [skipBirthdateCheck, setSkipBirthdateCheck] = useState(false);
// Définir le titre de la page
usePageTitle("Signature électronique");
// Définir le titre de la page dynamiquement
const documentTypeLabel = signatureData?.documentType === 'avenant' ? 'Avenant' : 'Contrat';
usePageTitle(`Signature électronique - ${documentTypeLabel}`);
// Vérifier si le contrat est déjà signé
const checkSignatureStatus = async () => {
@ -44,20 +46,22 @@ export default function SignatureSalarieContent() {
console.log('📊 Statut reçu:', data);
if (data.isSigned) {
console.log('✅ Contrat déjà signé');
console.log('✅ Document déjà signé');
setAlreadySigned(true);
setSignatureData({
contract: data.contract,
signed_at: data.signed_at,
downloadUrl: data.downloadUrl
downloadUrl: data.downloadUrl,
documentType: data.documentType || 'contrat' // Par défaut "contrat" si non spécifié
});
} else {
console.log('📝 Contrat pas encore signé');
console.log('📝 Document pas encore signé');
setAlreadySigned(false);
setSignatureData({
contract: data.contract,
signed_at: null,
downloadUrl: null
downloadUrl: null,
documentType: data.documentType || 'contrat'
});
}
} else {
@ -146,6 +150,7 @@ export default function SignatureSalarieContent() {
contract={signatureData.contract}
signed_at={signatureData.signed_at}
downloadUrl={signatureData.downloadUrl}
documentType={signatureData.documentType}
/>
);
}
@ -157,6 +162,7 @@ export default function SignatureSalarieContent() {
<ToSignCard
contract={signatureData.contract}
onSignClick={() => setShowSignatureModal(true)}
documentType={signatureData.documentType}
/>
<DocuSealSignatureModal

View file

@ -15,9 +15,11 @@ interface ToSignCardProps {
profession: string | null;
};
onSignClick: () => void;
documentType?: 'contrat' | 'avenant'; // Nouveau: type de document
}
export default function ToSignCard({ contract, onSignClick }: ToSignCardProps) {
export default function ToSignCard({ contract, onSignClick, documentType = 'contrat' }: ToSignCardProps) {
const documentLabel = documentType === 'avenant' ? 'Avenant' : 'Contrat';
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
@ -79,10 +81,10 @@ export default function ToSignCard({ contract, onSignClick }: ToSignCardProps) {
<AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-900">
Contrat en attente de signature
{documentLabel} en attente de signature
</p>
<p className="text-sm text-blue-700 mt-1">
Merci de bien vouloir prendre connaissance du contrat et de le signer électroniquement.
Merci de bien vouloir prendre connaissance {documentType === 'avenant' ? 'de l\'avenant' : 'du contrat'} et de le signer électroniquement.
</p>
</div>
</div>
@ -92,7 +94,7 @@ export default function ToSignCard({ contract, onSignClick }: ToSignCardProps) {
<div className="space-y-4">
<h2 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<FileText className="w-5 h-5 text-indigo-600" />
Détails du contrat
Détails {documentType === 'avenant' ? 'de l\'avenant' : 'du contrat'}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@ -188,7 +190,7 @@ export default function ToSignCard({ contract, onSignClick }: ToSignCardProps) {
className="w-full inline-flex items-center justify-center gap-3 px-6 py-4 text-base font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
>
<FileSignature className="w-5 h-5" />
Voir et signer votre contrat
Voir et signer votre {documentLabel.toLowerCase()}
</button>
</div>
</div>