(null);
const queryClient = useQueryClient();
@@ -166,6 +168,11 @@ export default function NAAPage() {
setIsCreateModalOpen(false);
};
+ const handleEditSuccess = () => {
+ queryClient.invalidateQueries({ queryKey: ["staff-naa-list"] });
+ setEditingNaaId(null);
+ };
+
const getStatusBadge = (status: string) => {
const badges = {
draft: "bg-slate-100 text-slate-700",
@@ -282,6 +289,14 @@ export default function NAAPage() {
+
setEditingNaaId(naa.id)}
+ disabled={loadingPdf === naa.id || regenerating === naa.id}
+ className="p-1.5 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Modifier la NAA"
+ >
+
+
{naa.pdf_url && (
<>
)}
+
+ {editingNaaId && (
+
setEditingNaaId(null)}
+ onSuccess={handleEditSuccess}
+ />
+ )}
);
}
diff --git a/app/api/contrats/[id]/generate-pdf-test/route.tsx b/app/api/contrats/[id]/generate-pdf-test/route.tsx
new file mode 100644
index 0000000..2c6b45d
--- /dev/null
+++ b/app/api/contrats/[id]/generate-pdf-test/route.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { NextRequest, NextResponse } from 'next/server';
+import { createClient } from '@supabase/supabase-js';
+import { generateContractPdf } from '@/lib/pdf/generateContract';
+import { ContratCDDUData } from '@/lib/pdf/types';
+
+/**
+ * Route API pour tester la génération de PDF d'un contrat existant
+ *
+ * URL: GET /api/contrats/[id]/generate-pdf-test
+ *
+ * Cette route récupère un contrat depuis Supabase et génère son PDF
+ */
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const contractId = params.id;
+ console.log('🧪 [Test PDF Generation] Génération du PDF pour le contrat:', contractId);
+
+ // 1. Récupérer les données du contrat depuis Supabase (avec service_role pour bypass RLS)
+ const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
+ {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+ }
+ );
+
+ const { data: contract, error: contractError } = await supabase
+ .from('cddu_contracts')
+ .select('*')
+ .eq('id', contractId)
+ .single();
+
+ if (contractError || !contract) {
+ console.error('❌ [Test PDF Generation] Contrat non trouvé:', contractError);
+ return NextResponse.json(
+ { error: 'Contrat non trouvé', details: contractError?.message },
+ { status: 404 }
+ );
+ }
+
+ console.log('✅ [Test PDF Generation] Contrat récupéré:', {
+ id: contract.id,
+ employee: contract.employee_name,
+ organization: contract.structure,
+ contract_number: contract.contract_number,
+ });
+
+ // 2. Transformer les données au format attendu par le template PDF
+ // La table cddu_contracts contient déjà toutes les données nécessaires
+ const pdfData: ContratCDDUData = {
+ // Structure employeur
+ structure_name: contract.structure || '',
+ structure_adresse: contract.structure_address || '',
+ structure_cpville: contract.structure_postal_code || '',
+ structure_ville: contract.structure_city || '',
+ structure_siret: contract.structure_siret || '',
+ structure_licence: contract.structure_license || '',
+ structure_signataire: contract.structure_signatory || '',
+ structure_signatairequalite: contract.structure_signatory_title || '',
+ structure_spectacle: contract.is_spectacle ? 'Oui' : 'Non',
+ delegation: contract.delegation || '',
+ forme_juridique: contract.legal_form || '',
+
+ // Représentant légal (mineur)
+ mineur1618: contract.is_minor ? 'Oui' : 'Non',
+ representant_civ: contract.guardian_civility || '',
+ representant_nom: contract.guardian_name || '',
+ representant_dob: contract.guardian_birth_date || '',
+ representant_cob: contract.guardian_birth_place || '',
+ representant_adresse: contract.guardian_address || '',
+
+ // Salarié - utiliser les champs qui existent vraiment dans cddu_contracts
+ employee_civ: contract.employee_civility || '',
+ employee_firstname: contract.employee_firstname || contract.prenom || '',
+ employee_lastname: contract.employee_lastname || contract.nom || '',
+ employee_birthname: contract.employee_birthname || contract.nomnaiss || '',
+ employee_dob: contract.employee_birth_date || contract.datedenaissancemois || '',
+ employee_cob: contract.employee_birth_place || contract.communedenaiss || '',
+ employee_address: contract.employee_address || contract.adressepostale || '',
+ employee_ss: contract.employee_ss_number || contract.secu || '',
+ employee_cs: contract.employee_classification || contract.cs || '',
+ employee_profession: contract.employee_profession || contract.profession || '',
+ employee_codeprofession: contract.employee_profession_code || contract.codeprofession || '',
+ employee_catpro: contract.employee_category || contract.catpro || '',
+ employee_pseudo: contract.employee_artistic_name || contract.pseudo || '',
+
+ // Spectacle/Production
+ spectacle: contract.production_name || contract.spectacle || '',
+ numobjet: contract.object_number || contract.numobjet || '',
+ type_numobjet: contract.object_type || contract.typedenumobjet || '',
+
+ // Dates et durée
+ date_debut: contract.start_date || contract.datedebutcontrat || '',
+ date_fin: contract.end_date || contract.datefincontrat || '',
+ dates_travaillees: contract.working_dates || contract.datestravaillees || '',
+ date_signature: contract.signature_date || new Date().toISOString().split('T')[0],
+
+ // Rémunération
+ salaire_brut: contract.gross_salary?.toString() || contract.brut || '0',
+ precisions_salaire: contract.salary_details || contract.precisionssalaire || '',
+ panierrepas: contract.meal_allowance || contract.panierrepas || '',
+ panierrepasccn: contract.meal_allowance_ccn ? 'Oui' : 'Non',
+ montantpanierrepas: contract.meal_allowance_amount?.toString() || contract.montantpanierrepas || '',
+ hebergement: contract.accommodation || contract.hebergement || '',
+ hebergementccn: contract.accommodation_ccn ? 'Oui' : 'Non',
+ montanthebergement: contract.accommodation_amount?.toString() || contract.montanthebergement || '',
+
+ // Cachets
+ cachets: {
+ representations: contract.cachets_representations || contract.representations || 0,
+ repetitions: contract.cachets_repetitions || contract.repetitions || 0,
+ heures: contract.cachets_hours || contract.heures || 0,
+ heuresparjour: contract.cachets_hours_per_day || contract.heuresparjour || 0,
+ },
+
+ // Convention collective
+ CCN: contract.collective_agreement || contract.ccn || contract.conventioncollective || '',
+
+ // Autres
+ autreprecision: contract.other_details || contract.autreprecision || '',
+ nom_responsable_traitement: contract.data_controller_name || '',
+ qualite_responsable_traitement: contract.data_controller_title || '',
+ email_responsable_traitement: contract.data_controller_email || '',
+ imageUrl: '', // Pas d'image pour le test
+ };
+
+ console.log('📄 [Test PDF Generation] Données transformées, génération du PDF...');
+
+ // 3. Générer le PDF
+ const pdfBuffer = await generateContractPdf(pdfData);
+
+ console.log(`✅ [Test PDF Generation] PDF généré avec succès (${pdfBuffer.byteLength} bytes)`);
+
+ // 4. Retourner le PDF
+ return new NextResponse(new Uint8Array(pdfBuffer), {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/pdf',
+ 'Content-Disposition': `inline; filename="contrat_${contractId}.pdf"`,
+ 'Content-Length': pdfBuffer.byteLength.toString(),
+ },
+ });
+
+ } catch (error) {
+ console.error('❌ [Test PDF Generation] Erreur:', error);
+
+ return NextResponse.json(
+ {
+ error: 'Erreur lors de la génération du PDF',
+ details: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/staff/naa/[id]/regenerate/route.ts b/app/api/staff/naa/[id]/regenerate/route.ts
index ea9773e..20190b3 100644
--- a/app/api/staff/naa/[id]/regenerate/route.ts
+++ b/app/api/staff/naa/[id]/regenerate/route.ts
@@ -79,6 +79,32 @@ export async function POST(
console.log(`[NAA Regenerate] Starting regeneration for NAA ID: ${params.id}`);
+ // Lire le body (optionnel) pour récupérer les factures explicitement incluses
+ let includedInvoiceIds: string[] = [];
+ try {
+ const reqBody = await request.json();
+ includedInvoiceIds = Array.isArray(reqBody?.included_invoices) ? reqBody.included_invoices : [];
+ } catch (e) {
+ // pas de body
+ includedInvoiceIds = [];
+ }
+
+ // Si des invoice IDs sont fournis, précharger ces factures
+ let includedInvoicesMap: Record = {};
+ if (includedInvoiceIds && includedInvoiceIds.length > 0) {
+ const { data: includedInvoices } = await supabase
+ .from('invoices')
+ .select('id, amount_ht, org_id, created_at, period_label')
+ .in('id', includedInvoiceIds as string[]);
+
+ if (includedInvoices && includedInvoices.length > 0) {
+ for (const inv of includedInvoices) {
+ includedInvoicesMap[inv.id] = inv;
+ }
+ }
+ console.log(`[NAA Regenerate] ${Object.keys(includedInvoicesMap).length} included invoices preloaded`);
+ }
+
// Récupérer le document NAA avec l'apporteur
const { data: naaDoc, error: naaError } = await supabase
.from("naa_documents")
@@ -93,23 +119,217 @@ export async function POST(
return NextResponse.json({ error: "NAA non trouvée" }, { status: 404 });
}
- // Récupérer les prestations
+ // Récupérer les prestations actuelles
const { data: prestations } = await supabase
.from("naa_prestations")
.select("*")
.eq("naa_id", params.id)
.order("created_at");
- // Récupérer les line items (commissions)
- const { data: lineItems } = await supabase
- .from("naa_line_items")
- .select("*")
- .eq("naa_id", params.id)
- .order("created_at");
-
const referrer = Array.isArray(naaDoc.referrers) ? naaDoc.referrers[0] : naaDoc.referrers;
- // Préparer le payload pour PDFMonkey
+ // ===== RECALCUL COMPLET DES COMMISSIONS =====
+ console.log("[NAA Regenerate] Recalculating commissions from scratch...");
+
+ // Extraire le mois et l'année de la période (format : "Janvier 2025")
+ const periodeParts = naaDoc.periode.split(" ");
+ const monthNames = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"];
+ const periodMonth = monthNames.indexOf(periodeParts[0]) + 1;
+ const periodYear = parseInt(periodeParts[1]);
+ const month = periodMonth.toString().padStart(2, '0');
+
+ // Calcul du mois d'émission (mois suivant)
+ let emissionMonth = periodMonth + 1;
+ let emissionYear = periodYear;
+
+ if (emissionMonth > 12) {
+ emissionMonth = 1;
+ emissionYear += 1;
+ }
+
+ const emissionMonthStr = emissionMonth.toString().padStart(2, '0');
+ const emissionStartDate = `${emissionYear}-${emissionMonthStr}-01`;
+ const emissionEndDate = `${emissionYear}-${emissionMonthStr}-20`;
+
+ // Récupérer la liste actuelle des clients apportés (peut avoir changé)
+ const { data: referredClients, error: clientsError } = await supabase
+ .from("organization_details")
+ .select(`
+ org_id,
+ referrer_code,
+ commission_rate,
+ code_employeur,
+ organizations!organization_details_org_id_fkey (
+ id,
+ name
+ )
+ `)
+ .eq("is_referred", true)
+ .eq("referrer_code", naaDoc.referrer_code);
+
+ if (clientsError) {
+ console.error("[NAA Regenerate] Erreur récupération clients apportés:", clientsError);
+ return NextResponse.json(
+ { error: "Erreur lors de la récupération des clients" },
+ { status: 500 }
+ );
+ }
+
+ console.log(`[NAA Regenerate] Trouvé ${referredClients?.length || 0} clients apportés actuellement`);
+
+ // Recalculer les line items pour chaque client apporté
+ const newLineItems = [];
+ let totalCommission = 0;
+
+ for (const client of referredClients || []) {
+ const org = Array.isArray(client.organizations) ? client.organizations[0] : client.organizations;
+ const clientCode = client.code_employeur;
+ const clientName = org?.name || 'N/A';
+
+ console.log(`[NAA Regenerate] Recherche facture pour client ${clientName} (${clientCode})`);
+
+ // Si l'utilisateur a fourni des invoices incluses, vérifier s'il y en a pour ce client
+ let invoice: any | null = null;
+ if (includedInvoiceIds && includedInvoiceIds.length > 0) {
+ const matching = Object.values(includedInvoicesMap).filter((inv: any) => String(inv.org_id) === String(client.org_id));
+ if (matching && matching.length > 0) {
+ // prendre la plus récente
+ invoice = matching.sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
+ console.log(`[NAA Regenerate] ✅ Utilisation d'une facture explicitement incluse: ${invoice.id}`);
+ }
+ }
+
+ // Stratégie 1 : Recherche exacte par period_label (si pas de facture fournie)
+ if (!invoice) {
+ invoice = await supabase
+ .from("invoices")
+ .select("id, amount_ht, created_at, period_label")
+ .eq("org_id", client.org_id)
+ .eq("period_label", naaDoc.periode)
+ .order("created_at", { ascending: false })
+ .limit(1)
+ .maybeSingle()
+ .then(res => res.data);
+ }
+
+ // Stratégie 2 : Si pas trouvé, chercher par date de création
+ if (!invoice) {
+ invoice = await supabase
+ .from("invoices")
+ .select("id, amount_ht, created_at, period_label")
+ .eq("org_id", client.org_id)
+ .gte("created_at", emissionStartDate)
+ .lte("created_at", emissionEndDate)
+ .order("created_at", { ascending: false })
+ .limit(1)
+ .maybeSingle()
+ .then(res => res.data);
+ }
+
+ // Stratégie 3 : Chercher dans une plage élargie
+ if (!invoice) {
+ const { data: invoices } = await supabase
+ .from("invoices")
+ .select("id, amount_ht, created_at, period_label")
+ .eq("org_id", client.org_id)
+ .gte("created_at", `${periodYear}-${month}-01`)
+ .lte("created_at", `${emissionYear}-${emissionMonthStr}-31`)
+ .order("created_at", { ascending: false });
+
+ if (invoices && invoices.length > 0) {
+ invoice = invoices[0];
+ }
+ }
+
+ if (invoice && invoice.amount_ht > 0) {
+ const caHT = parseFloat(invoice.amount_ht);
+ const commissionRate = client.commission_rate || 0;
+ const commission = caHT * commissionRate;
+ totalCommission += commission;
+
+ console.log(`[NAA Regenerate] ✅ Commission: ${caHT}€ × ${(commissionRate * 100).toFixed(2)}% = ${commission.toFixed(2)}€`);
+
+ newLineItems.push({
+ naa_id: params.id,
+ organization_id: client.org_id,
+ client_name: clientName,
+ client_code: clientCode,
+ invoice_id: invoice.id || null,
+ commission_rate: commissionRate,
+ ca_ht: caHT,
+ commission: commission
+ });
+ } else {
+ console.log(`[NAA Regenerate] ⚠️ Aucune facture trouvée ou montant HT = 0`);
+ }
+ }
+
+ // Supprimer les anciens line items et insérer les nouveaux
+ await supabase
+ .from("naa_line_items")
+ .delete()
+ .eq("naa_id", params.id);
+
+ if (newLineItems.length > 0) {
+ await supabase
+ .from("naa_line_items")
+ .insert(newLineItems);
+ }
+
+ // Recalculer les totaux
+ const nbreClients = newLineItems.length;
+ const nbrePrestations = (prestations || []).length;
+ const totalFacture = totalCommission - (naaDoc.deposit || 0) + (naaDoc.solde_compte_apporteur || 0);
+
+ console.log(`[NAA Regenerate] Totaux recalculés:`);
+ console.log(` - Nombre de clients: ${nbreClients}`);
+ console.log(` - Nombre de prestations: ${nbrePrestations}`);
+ console.log(` - Total commission: ${totalCommission.toFixed(2)}€`);
+ console.log(` - Total facture: ${totalFacture.toFixed(2)}€`);
+
+ // Mettre à jour le document NAA avec les nouveaux totaux
+ await supabase
+ .from("naa_documents")
+ .update({
+ total_commission: totalCommission,
+ total_facture: totalFacture,
+ nbre_clients: nbreClients,
+ nbre_prestations: nbrePrestations,
+ updated_at: new Date().toISOString()
+ })
+ .eq("id", params.id);
+
+ // Trier les line items par ordre alphabétique de code client
+ const sortedLineItems = (newLineItems || [])
+ .map((item: any) => ({
+ id: item.client_code || '',
+ client: item.client_name || '',
+ code: item.client_code || '',
+ comactuelle: item.commission_rate || 0,
+ caht: item.ca_ht || 0,
+ commission: item.commission || 0
+ }))
+ .sort((a: any, b: any) => {
+ const codeA = a.code.toLowerCase();
+ const codeB = b.code.toLowerCase();
+ return codeA.localeCompare(codeB);
+ }); // Trier les prestations par ordre alphabétique du code client
+ const sortedPrestations = (prestations || [])
+ .map(p => ({
+ client: p.client_name,
+ code: p.client_code,
+ type_prestation: p.type_prestation,
+ quantite: p.quantite,
+ tarif: p.tarif,
+ total: p.total
+ }))
+ .sort((a, b) => {
+ const codeA = (a.code || "").toUpperCase();
+ const codeB = (b.code || "").toUpperCase();
+ return codeA.localeCompare(codeB);
+ });
+
+ // Préparer le payload pour PDFMonkey avec les totaux recalculés
const pdfMonkeyPayload = {
apporteur_address: referrer?.address || "",
apporteur_cp: referrer?.postal_code || "",
@@ -121,33 +341,20 @@ export async function POST(
limit_date: naaDoc.limit_date ? new Date(naaDoc.limit_date).toLocaleDateString("fr-FR") : "",
callsheet_number: naaDoc.naa_number,
periode: naaDoc.periode,
- total_commission: naaDoc.total_commission || 0,
+ total_commission: totalCommission,
solde_compte_apporteur: naaDoc.solde_compte_apporteur || 0,
- total_facture: naaDoc.total_facture || 0,
+ total_facture: totalFacture,
deposit: naaDoc.deposit || 0,
- nbre_clients: naaDoc.nbre_clients || 0,
- nbre_prestations: naaDoc.nbre_prestations || 0,
+ nbre_clients: nbreClients,
+ nbre_prestations: nbrePrestations,
transfer_reference: naaDoc.transfer_reference || "",
logo_odentas: "",
- lineItems: (lineItems || []).map(item => ({
- id: item.client_code,
- client: item.client_name,
- code: item.client_code,
- comactuelle: item.commission_rate,
- caht: item.ca_ht,
- commission: item.commission
- })),
- prestations: (prestations || []).map(p => ({
- client: p.client_name,
- code: p.client_code,
- type_prestation: p.type_prestation,
- quantite: p.quantite,
- tarif: p.tarif,
- total: p.total
- }))
+ lineItems: sortedLineItems,
+ prestations: sortedPrestations
};
console.log("[NAA Regenerate] Calling PDFMonkey API...");
+ console.log("[NAA Regenerate] Payload lineItems:", JSON.stringify(sortedLineItems, null, 2));
const pdfMonkeyApiKey = process.env.PDFMONKEY_API_KEY;
const pdfMonkeyUrl = "https://api.pdfmonkey.io/api/v1/documents";
@@ -227,13 +434,13 @@ export async function POST(
});
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 3600 });
- // Mettre à jour le document NAA
+ // Mettre à jour le document NAA avec l'URL du PDF (les totaux ont déjà été mis à jour plus haut)
await supabase
.from("naa_documents")
.update({
pdf_url: s3Url,
s3_key: s3Key,
- updated_at: new Date().toISOString()
+ status: "sent"
})
.eq("id", params.id);
diff --git a/app/api/staff/naa/[id]/route.ts b/app/api/staff/naa/[id]/route.ts
index ee4680c..12195e0 100644
--- a/app/api/staff/naa/[id]/route.ts
+++ b/app/api/staff/naa/[id]/route.ts
@@ -111,3 +111,156 @@ export async function DELETE(
);
}
}
+
+// PUT - Mettre à jour une NAA et ses prestations
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const cookieStore = cookies();
+ const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
+
+ // Vérifier l'authentification staff
+ const { data: { user } } = await supabase.auth.getUser();
+ if (!user) {
+ return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
+ }
+
+ const { data: staffUser } = await supabase
+ .from("staff_users")
+ .select("user_id")
+ .eq("user_id", user.id)
+ .single();
+
+ if (!staffUser) {
+ return NextResponse.json({ error: "Accès non autorisé" }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const {
+ prestations,
+ solde_compte_apporteur,
+ deposit,
+ included_invoices
+ } = body;
+
+ // Vérifier que la NAA existe
+ const { data: naaDoc, error: naaCheckError } = await supabase
+ .from("naa_documents")
+ .select("*")
+ .eq("id", params.id)
+ .single();
+
+ if (naaCheckError || !naaDoc) {
+ return NextResponse.json({ error: "NAA non trouvée" }, { status: 404 });
+ }
+
+ // 1. Récupérer les IDs des prestations existantes
+ const { data: existingPrestations } = await supabase
+ .from("naa_prestations")
+ .select("id")
+ .eq("naa_id", params.id);
+
+ const existingIds = existingPrestations?.map(p => p.id) || [];
+
+ // 2. Identifier les prestations à conserver (celles qui ont un ID existant)
+ const prestationsWithIds = prestations.filter((p: any) => p.id && existingIds.includes(p.id));
+ const prestationIdsToKeep = prestationsWithIds.map((p: any) => p.id);
+
+ // 3. Supprimer les prestations qui ne sont plus dans la liste
+ const idsToDelete = existingIds.filter(id => !prestationIdsToKeep.includes(id));
+
+ if (idsToDelete.length > 0) {
+ await supabase
+ .from("naa_prestations")
+ .delete()
+ .in("id", idsToDelete);
+ }
+
+ // 4. Mettre à jour les prestations existantes qui ont changé
+ for (const prest of prestationsWithIds) {
+ await supabase
+ .from("naa_prestations")
+ .update({
+ client_name: prest.client,
+ client_code: prest.code,
+ type_prestation: prest.type_prestation,
+ quantite: prest.quantite,
+ tarif: prest.tarif,
+ total: prest.total
+ })
+ .eq("id", prest.id);
+ }
+
+ // 5. Insérer les nouvelles prestations (celles sans ID)
+ const newPrestations = prestations.filter((p: any) => !p.id);
+
+ if (newPrestations.length > 0) {
+ const prestationsToInsert = newPrestations.map((p: any) => ({
+ naa_id: params.id,
+ client_name: p.client,
+ client_code: p.code,
+ type_prestation: p.type_prestation,
+ quantite: p.quantite,
+ tarif: p.tarif,
+ total: p.total
+ }));
+
+ await supabase.from("naa_prestations").insert(prestationsToInsert);
+ }
+
+ // 6. Calculer les nouveaux totaux
+ const nbrePrestations = prestations.length;
+ const uniqueClients = [...new Set(prestations.map((p: any) => p.code))];
+ const nbreClients = uniqueClients.length;
+
+ // 7. Mettre à jour le document NAA avec les nouveaux totaux
+ await supabase
+ .from("naa_documents")
+ .update({
+ nbre_prestations: nbrePrestations,
+ nbre_clients: nbreClients,
+ solde_compte_apporteur: solde_compte_apporteur || 0,
+ deposit: deposit || 0,
+ updated_at: new Date().toISOString(),
+ status: "draft" // Remettre en draft car le PDF doit être régénéré
+ })
+ .eq("id", params.id);
+
+ // 8. Régénérer le PDF
+ const regenerateRes = await fetch(
+ `${request.nextUrl.origin}/api/staff/naa/${params.id}/regenerate`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ cookie: request.headers.get("cookie") || ""
+ },
+ body: JSON.stringify({ included_invoices })
+ }
+ );
+
+ if (!regenerateRes.ok) {
+ console.error("Erreur lors de la régénération du PDF");
+ return NextResponse.json({
+ success: true,
+ warning: "NAA mise à jour mais erreur lors de la régénération du PDF"
+ });
+ }
+
+ const regenerateData = await regenerateRes.json();
+
+ return NextResponse.json({
+ success: true,
+ presigned_url: regenerateData.presigned_url
+ });
+
+ } catch (error: any) {
+ console.error("Error PUT /api/staff/naa/[id]:", error);
+ return NextResponse.json(
+ { error: error.message || "Erreur serveur" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/staff/naa/generate/route.ts b/app/api/staff/naa/generate/route.ts
index 4b9994d..94fe154 100644
--- a/app/api/staff/naa/generate/route.ts
+++ b/app/api/staff/naa/generate/route.ts
@@ -147,9 +147,9 @@ export async function POST(req: NextRequest) {
const emissionMonthStr = emissionMonth.toString().padStart(2, '0');
- // Date de début et fin du mois d'émission (1er au 10 du mois suivant généralement)
+ // Date de début et fin du mois d'émission (1er au 20 du mois suivant)
const emissionStartDate = `${emissionYear}-${emissionMonthStr}-01`;
- const emissionEndDate = `${emissionYear}-${emissionMonthStr}-15`; // Buffer de 15 jours
+ const emissionEndDate = `${emissionYear}-${emissionMonthStr}-20`;
// Récupérer les clients apportés
const { data: referredClients, error: clientsError } = await supabase
@@ -346,6 +346,29 @@ export async function POST(req: NextRequest) {
await supabase.from("naa_prestations").insert(prestationsToInsert);
}
+ // Trier les lineItems par ordre alphabétique du code client
+ const sortedLineItems = [...lineItems].sort((a, b) => {
+ const codeA = (a.code || "").toUpperCase();
+ const codeB = (b.code || "").toUpperCase();
+ return codeA.localeCompare(codeB);
+ });
+
+ // Trier les prestations par ordre alphabétique du code client
+ const sortedPrestations = [...prestations]
+ .map(p => ({
+ client: p.client,
+ code: p.code,
+ type_prestation: p.type_prestation,
+ quantite: p.quantite,
+ tarif: p.tarif,
+ total: p.total
+ }))
+ .sort((a, b) => {
+ const codeA = (a.code || "").toUpperCase();
+ const codeB = (b.code || "").toUpperCase();
+ return codeA.localeCompare(codeB);
+ });
+
// Préparer les données pour PDFMonkey
const pdfMonkeyPayload = {
apporteur_address: referrer.address,
@@ -366,15 +389,8 @@ export async function POST(req: NextRequest) {
nbre_prestations: nbrePrestations,
transfer_reference: transfer_reference || "",
logo_odentas: "",
- lineItems,
- prestations: prestations.map(p => ({
- client: p.client,
- code: p.code,
- type_prestation: p.type_prestation,
- quantite: p.quantite,
- tarif: p.tarif,
- total: p.total
- }))
+ lineItems: sortedLineItems,
+ prestations: sortedPrestations
};
// Envoyer à PDFMonkey pour générer le PDF
diff --git a/app/api/staff/organizations/[orgId]/invoices/route.ts b/app/api/staff/organizations/[orgId]/invoices/route.ts
new file mode 100644
index 0000000..57dc90e
--- /dev/null
+++ b/app/api/staff/organizations/[orgId]/invoices/route.ts
@@ -0,0 +1,80 @@
+import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
+import { cookies } from "next/headers";
+import { NextRequest, NextResponse } from "next/server";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(
+ req: NextRequest,
+ { params }: { params: { orgId: string } }
+) {
+ try {
+ const supabase = createRouteHandlerClient({ cookies });
+
+ const { data: { session } } = await supabase.auth.getSession();
+ if (!session) {
+ return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
+ }
+
+ // Vérifier que l'utilisateur est staff
+ const { data: staffUser } = await supabase
+ .from("staff_users")
+ .select("is_staff")
+ .eq("user_id", session.user.id)
+ .single();
+
+ if (!staffUser || !staffUser.is_staff) {
+ return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
+ }
+
+ const periode = req.nextUrl.searchParams.get("periode") || undefined;
+
+ // Si une période est fournie, on va élargir la recherche sur 2 mois autour
+ let startDate: string | undefined;
+ let endDate: string | undefined;
+
+ if (periode) {
+ const parts = periode.split(" ");
+ const monthNames = ["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"];
+ const periodMonth = monthNames.indexOf(parts[0]) + 1;
+ const periodYear = parseInt(parts[1]);
+ const month = periodMonth.toString().padStart(2, '0');
+
+ let emissionMonth = periodMonth + 1;
+ let emissionYear = periodYear;
+ if (emissionMonth > 12) { emissionMonth = 1; emissionYear += 1; }
+ const emissionMonthStr = String(emissionMonth).padStart(2, '0');
+
+ startDate = `${periodYear}-${month}-01`;
+ endDate = `${emissionYear}-${emissionMonthStr}-20`;
+ }
+
+ const orgId = params.orgId;
+
+ let query = supabase
+ .from("invoices")
+ .select("id, amount_ht, created_at, period_label, org_id")
+ .eq("org_id", orgId)
+ .order("created_at", { ascending: false })
+ .limit(50);
+
+ if (startDate && endDate) {
+ query = query.gte("created_at", startDate).lte("created_at", endDate) as any;
+ }
+
+ const { data: invoices, error } = await query;
+
+ if (error) {
+ console.error("Erreur GET invoices for org:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+
+ return NextResponse.json(invoices || []);
+ } catch (error: any) {
+ console.error("Erreur GET /api/staff/organizations/[orgId]/invoices:", error);
+ return NextResponse.json(
+ { error: error.message || "Erreur serveur" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/test-pdf/route.tsx b/app/api/test-pdf/route.tsx
new file mode 100644
index 0000000..c661788
--- /dev/null
+++ b/app/api/test-pdf/route.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { NextRequest, NextResponse } from 'next/server';
+import ReactPDF from '@react-pdf/renderer';
+import { ContratCDDU } from '@/lib/pdf/templates/ContratCDDU';
+import testData from '@/PDF/donnees_test.json';
+import { ContratCDDUData } from '@/lib/pdf/types';
+
+/**
+ * Route API de test pour la génération de PDF avec @react-pdf/renderer
+ *
+ * URL: GET /api/test-pdf
+ *
+ * Cette route génère un PDF de contrat CDDU en utilisant les données de test
+ * et retourne le PDF directement dans le navigateur.
+ */
+export async function GET(request: NextRequest) {
+ try {
+ console.log('🧪 [test-pdf] Début de la génération du PDF de test');
+
+ // Cast des données de test
+ const data = testData as unknown as ContratCDDUData;
+
+ // Génération du PDF à partir du composant React
+ console.log('📄 [test-pdf] Rendu du composant ContratCDDU...');
+
+ // Créer le composant et le rendre en PDF
+ const doc = ;
+ const pdfBlob = await ReactPDF.pdf(doc).toBlob();
+
+ // Convertir le Blob en ArrayBuffer puis en Buffer
+ const arrayBuffer = await pdfBlob.arrayBuffer();
+ const pdfBuffer = Buffer.from(arrayBuffer);
+
+ console.log(`✅ [test-pdf] PDF généré avec succès (${pdfBuffer.byteLength} bytes)`);
+
+ // Retour du PDF avec les bons headers
+ return new NextResponse(pdfBuffer, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/pdf',
+ 'Content-Disposition': 'inline; filename="contrat_cddu_test.pdf"',
+ 'Content-Length': pdfBuffer.byteLength.toString(),
+ },
+ });
+
+ } catch (error) {
+ console.error('❌ [test-pdf] Erreur lors de la génération du PDF:', error);
+
+ return NextResponse.json(
+ {
+ error: 'Erreur lors de la génération du PDF',
+ details: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/components/staff/EditNAAModal.tsx b/components/staff/EditNAAModal.tsx
new file mode 100644
index 0000000..a815666
--- /dev/null
+++ b/components/staff/EditNAAModal.tsx
@@ -0,0 +1,712 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { X, Plus, Trash2, ChevronDown, ChevronRight, Loader2 } from "lucide-react";
+
+type PrestationLine = {
+ id?: string; // ID de la prestation existante (pour la suppression)
+ type_prestation: string;
+ quantite: number;
+ tarif: number;
+ total: number;
+};
+
+type ClientPrestations = {
+ client: string;
+ code: string;
+ expanded: boolean;
+ lines: PrestationLine[];
+};
+
+type ReferredClient = {
+ org_id: string;
+ code_employeur: string;
+ organizations: {
+ name: string;
+ };
+};
+
+type Invoice = {
+ id: string;
+ amount_ht: string;
+ created_at: string;
+ period_label?: string;
+ org_id: string;
+};
+
+type EditNAAModalProps = {
+ naaId: string;
+ onClose: () => void;
+ onSuccess: () => void;
+};
+
+export default function EditNAAModal({ naaId, onClose, onSuccess }: EditNAAModalProps) {
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Formulaire
+ const [referrerCode, setReferrerCode] = useState("");
+ const [periode, setPeriode] = useState("");
+ const [callsheetDate, setCallsheetDate] = useState("");
+ const [limitDate, setLimitDate] = useState("");
+ const [transferReference, setTransferReference] = useState("");
+ const [soldeCompte, setSoldeCompte] = useState("0");
+ const [deposit, setDeposit] = useState("0");
+
+ // Prestations groupées par client
+ const [clientsPrestations, setClientsPrestations] = useState([]);
+ // Factures détectées par organisation
+ const [clientInvoices, setClientInvoices] = useState>({});
+ // Ensemble des invoices sélectionnées (IDs)
+ const [selectedInvoiceIds, setSelectedInvoiceIds] = useState>({});
+ // État d'expansion des sections factures par org_id
+ const [invoicesExpanded, setInvoicesExpanded] = useState>({});
+
+ const typesPrestation = [
+ "Ouverture de compte",
+ "Abonnement",
+ "Paies CDDU",
+ "Paies RG",
+ "Avenants",
+ "Autres"
+ ];
+
+ // Charger les données de la NAA existante
+ const { data: naaData, isLoading: isLoadingNaa } = useQuery({
+ queryKey: ["naa-detail", naaId],
+ queryFn: async () => {
+ const res = await fetch(`/api/staff/naa/${naaId}`, {
+ credentials: "include"
+ });
+ if (!res.ok) throw new Error("Impossible de charger la NAA");
+ return res.json();
+ },
+ });
+
+ // Charger les clients apportés
+ const { data: referredClients = [] } = useQuery({
+ queryKey: ["referred-clients", referrerCode],
+ queryFn: async () => {
+ if (!referrerCode) return [];
+ const res = await fetch(`/api/staff/referrers/${referrerCode}/clients`, {
+ credentials: "include"
+ });
+ if (!res.ok) return [];
+ return res.json();
+ },
+ enabled: !!referrerCode,
+ });
+
+ // Initialiser le formulaire avec les données existantes
+ useEffect(() => {
+ if (naaData) {
+ setReferrerCode(naaData.referrer_code || "");
+ setPeriode(naaData.periode || "");
+ setCallsheetDate(naaData.callsheet_date || "");
+ setLimitDate(naaData.limit_date || "");
+ setTransferReference(naaData.transfer_reference || "");
+ setSoldeCompte(String(naaData.solde_compte_apporteur || 0));
+ setDeposit(String(naaData.deposit || 0));
+
+ // Regrouper les prestations par client
+ if (naaData.prestations && naaData.prestations.length > 0) {
+ const grouped = naaData.prestations.reduce((acc: any, prest: any) => {
+ const key = `${prest.client_code}_${prest.client_name}`;
+ if (!acc[key]) {
+ acc[key] = {
+ client: prest.client_name,
+ code: prest.client_code,
+ expanded: false, // Replié par défaut
+ lines: []
+ };
+ }
+ acc[key].lines.push({
+ id: prest.id,
+ type_prestation: prest.type_prestation,
+ quantite: prest.quantite,
+ tarif: prest.tarif,
+ total: prest.total
+ });
+ return acc;
+ }, {});
+
+ setClientsPrestations(Object.values(grouped));
+ }
+ }
+ }, [naaData]);
+
+ // Si des clients sont déjà présents (à l'ouverture), précharger leurs factures
+ useEffect(() => {
+ const preload = async () => {
+ for (const client of clientsPrestations) {
+ if (client.code) {
+ const rc = referredClients.find(rc => rc.code_employeur === client.code);
+ if (rc && rc.org_id && !(clientInvoices[rc.org_id] || []).length) {
+ try {
+ const res = await fetch(`/api/staff/organizations/${rc.org_id}/invoices?periode=${encodeURIComponent(periode)}`, { credentials: 'include' });
+ if (!res.ok) continue;
+ const invoices = await res.json();
+ setClientInvoices(prev => ({ ...prev, [rc.org_id]: invoices }));
+ if (invoices && invoices.length > 0) {
+ setSelectedInvoiceIds(prev => ({ ...prev, [rc.org_id]: [invoices[0].id] }));
+ }
+ } catch (e) {
+ // ignore
+ }
+ }
+ }
+ }
+ };
+ if (clientsPrestations.length > 0 && referredClients.length > 0) {
+ preload();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [clientsPrestations, referredClients]);
+
+ // Ajouter un nouveau client
+ const addClient = () => {
+ setClientsPrestations([...clientsPrestations, {
+ client: "",
+ code: "",
+ expanded: true, // Nouveau client déplié pour faciliter la saisie
+ lines: [{
+ type_prestation: "",
+ quantite: 1,
+ tarif: 0,
+ total: 0
+ }]
+ }]);
+ };
+
+ // Supprimer un client et toutes ses prestations
+ const removeClient = (clientIndex: number) => {
+ setClientsPrestations(clientsPrestations.filter((_, i) => i !== clientIndex));
+ };
+
+ // Ajouter une ligne de prestation pour un client
+ const addLineToClient = (clientIndex: number) => {
+ const newClients = [...clientsPrestations];
+ newClients[clientIndex].lines.push({
+ type_prestation: "",
+ quantite: 1,
+ tarif: 0,
+ total: 0
+ });
+ setClientsPrestations(newClients);
+ };
+
+ // Supprimer une ligne de prestation
+ const removeLine = (clientIndex: number, lineIndex: number) => {
+ const newClients = [...clientsPrestations];
+ newClients[clientIndex].lines = newClients[clientIndex].lines.filter((_, i) => i !== lineIndex);
+ setClientsPrestations(newClients);
+ };
+
+ // Mettre à jour le client
+ const updateClient = (clientIndex: number, clientName: string) => {
+ const newClients = [...clientsPrestations];
+ const selectedClient = referredClients.find(c => c.organizations.name === clientName);
+ newClients[clientIndex].client = clientName;
+ newClients[clientIndex].code = selectedClient?.code_employeur || "";
+ setClientsPrestations(newClients);
+
+ // Si on a trouvé l'org_id, récupérer les factures candidates
+ if (selectedClient?.org_id) {
+ fetch(`/api/staff/organizations/${selectedClient.org_id}/invoices?periode=${encodeURIComponent(periode)}`, {
+ credentials: "include"
+ })
+ .then(res => res.ok ? res.json() : [])
+ .then((invoices: Invoice[]) => {
+ setClientInvoices(prev => ({ ...prev, [selectedClient.org_id]: invoices }));
+ // Par défaut, sélectionner la facture la plus récente (la première)
+ if (invoices && invoices.length > 0) {
+ setSelectedInvoiceIds(prev => ({ ...prev, [selectedClient.org_id]: [invoices[0].id] }));
+ }
+ })
+ .catch(() => {});
+ }
+ };
+
+ const toggleInvoiceSelection = (orgId: string, invoiceId: string) => {
+ setSelectedInvoiceIds(prev => {
+ const current = prev[orgId] || [];
+ // Si cette facture est déjà la seule sélectionnée, on la désélectionne
+ if (current.length === 1 && current[0] === invoiceId) {
+ return { ...prev, [orgId]: [] };
+ }
+ // Sinon, on remplace la sélection par cette facture uniquement (mode radio)
+ return { ...prev, [orgId]: [invoiceId] };
+ });
+ };
+
+ // Mettre à jour une ligne de prestation
+ const updateLine = (clientIndex: number, lineIndex: number, field: keyof PrestationLine, value: any) => {
+ const newClients = [...clientsPrestations];
+ newClients[clientIndex].lines[lineIndex] = {
+ ...newClients[clientIndex].lines[lineIndex],
+ [field]: value
+ };
+
+ // Calculer le total automatiquement
+ if (field === "quantite" || field === "tarif") {
+ const line = newClients[clientIndex].lines[lineIndex];
+ const quantite = field === "quantite" ? parseFloat(value) || 0 : line.quantite;
+ const tarif = field === "tarif" ? parseFloat(value) || 0 : line.tarif;
+ newClients[clientIndex].lines[lineIndex].total = quantite * tarif;
+ }
+
+ setClientsPrestations(newClients);
+ };
+
+ // Basculer l'état expanded d'un client
+ const toggleClientExpanded = (clientIndex: number) => {
+ const newClients = [...clientsPrestations];
+ newClients[clientIndex].expanded = !newClients[clientIndex].expanded;
+ setClientsPrestations(newClients);
+ };
+
+ // Tout replier
+ const collapseAll = () => {
+ setClientsPrestations(clientsPrestations.map(c => ({ ...c, expanded: false })));
+ };
+
+ // Tout déplier
+ const expandAll = () => {
+ setClientsPrestations(clientsPrestations.map(c => ({ ...c, expanded: true })));
+ };
+
+ // Basculer l'expansion de la section factures
+ const toggleInvoicesExpanded = (orgId: string) => {
+ setInvoicesExpanded(prev => ({ ...prev, [orgId]: !prev[orgId] }));
+ };
+
+ // Calculer le total pour un client
+ const getClientTotal = (client: ClientPrestations) => {
+ return client.lines.reduce((sum, line) => sum + line.total, 0);
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError(null);
+
+ if (!referrerCode || !periode || !callsheetDate) {
+ setError("Veuillez remplir tous les champs obligatoires");
+ return;
+ }
+
+ setIsUpdating(true);
+
+ try {
+ // Convertir clientsPrestations en format plat pour l'API
+ const prestations = clientsPrestations.flatMap(client =>
+ client.lines.map(line => ({
+ id: line.id, // Inclure l'ID pour identifier les prestations existantes
+ client: client.client,
+ code: client.code,
+ type_prestation: line.type_prestation,
+ quantite: line.quantite,
+ tarif: line.tarif,
+ total: line.total
+ }))
+ );
+
+ const payload = {
+ referrer_code: referrerCode,
+ periode,
+ callsheet_date: callsheetDate,
+ limit_date: limitDate || undefined,
+ transfer_reference: transferReference || undefined,
+ solde_compte_apporteur: parseFloat(soldeCompte) || 0,
+ deposit: parseFloat(deposit) || 0,
+ prestations,
+ // Liste des factures explicitement incluses par l'utilisateur (IDs)
+ included_invoices: Object.values(selectedInvoiceIds).flat()
+ };
+
+ const res = await fetch(`/api/staff/naa/${naaId}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(payload)
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.error || "Erreur lors de la mise à jour de la NAA");
+ }
+
+ onSuccess();
+ } catch (err: any) {
+ setError(err.message);
+ setIsUpdating(false);
+ }
+ };
+
+ if (isLoadingNaa) {
+ return (
+
+
+
+ Chargement de la NAA...
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Modifier la NAA
+
+
+ {naaData?.naa_number} - {naaData?.periode}
+
+
+
+
+
+
+
+ {/* Body */}
+
+
+
+ );
+}
diff --git a/lib/pdf/generateContract.tsx b/lib/pdf/generateContract.tsx
new file mode 100644
index 0000000..21deb7d
--- /dev/null
+++ b/lib/pdf/generateContract.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import ReactPDF from '@react-pdf/renderer';
+import { ContratCDDU } from './templates/ContratCDDU';
+import { ContratCDDUData } from './types';
+import { uploadPdfToS3, generateContractS3Key } from './uploadPdf';
+
+export interface GenerateContractResult {
+ /** Clé S3 du fichier uploadé */
+ s3Key: string;
+ /** URL complète S3 (non signée) */
+ s3Url: string;
+ /** Taille du PDF en bytes */
+ size: number;
+}
+
+/**
+ * Génère un contrat CDDU en PDF et l'upload sur S3
+ *
+ * @param data - Données du contrat
+ * @param organizationId - ID de l'organisation
+ * @param contractId - ID du contrat
+ * @returns Informations sur le fichier uploadé
+ *
+ * @example
+ * ```typescript
+ * const result = await generateAndUploadContract(
+ * contractData,
+ * 'org-123',
+ * 'contract-456'
+ * );
+ *
+ * console.log('PDF uploadé:', result.s3Key);
+ * // Enregistrer result.s3Key dans Supabase
+ * ```
+ */
+export async function generateAndUploadContract(
+ data: ContratCDDUData,
+ organizationId: string,
+ contractId: string
+): Promise {
+ console.log('🚀 [Contract Generation] Début de la génération du contrat:', {
+ organizationId,
+ contractId,
+ });
+
+ try {
+ // 1. Générer le PDF
+ console.log('📄 [Contract Generation] Génération du PDF...');
+ const doc = ;
+ const pdfBlob = await ReactPDF.pdf(doc).toBlob();
+
+ // Convertir le Blob en Buffer
+ const arrayBuffer = await pdfBlob.arrayBuffer();
+ const pdfBuffer = Buffer.from(arrayBuffer);
+
+ console.log(`✅ [Contract Generation] PDF généré (${pdfBuffer.byteLength} bytes)`);
+
+ // 2. Générer la clé S3
+ const year = new Date(data.date_debut).getFullYear();
+ const s3Key = generateContractS3Key(organizationId, contractId, year);
+
+ // 3. Upload sur S3
+ console.log('📤 [Contract Generation] Upload sur S3...');
+ await uploadPdfToS3({
+ pdfBuffer,
+ key: s3Key,
+ metadata: {
+ contractId,
+ organizationId,
+ employeeName: `${data.employee_firstname} ${data.employee_lastname}`,
+ contractType: 'CDDU',
+ generatedAt: new Date().toISOString(),
+ },
+ });
+
+ // 4. Construire l'URL S3
+ const region = process.env.AWS_REGION || 'eu-west-3';
+ const bucket = process.env.AWS_S3_BUCKET || 'odentas-docs';
+ const s3Url = `https://${bucket}.s3.${region}.amazonaws.com/${s3Key}`;
+
+ console.log('✅ [Contract Generation] Contrat généré et uploadé avec succès:', {
+ s3Key,
+ s3Url,
+ size: pdfBuffer.byteLength,
+ });
+
+ return {
+ s3Key,
+ s3Url,
+ size: pdfBuffer.byteLength,
+ };
+ } catch (error) {
+ console.error('❌ [Contract Generation] Erreur lors de la génération du contrat:', error);
+ throw new Error(
+ `Échec de la génération du contrat: ${error instanceof Error ? error.message : String(error)}`
+ );
+ }
+}
+
+/**
+ * Génère uniquement le PDF sans l'uploader (utile pour les tests)
+ *
+ * @param data - Données du contrat
+ * @returns Buffer du PDF
+ */
+export async function generateContractPdf(data: ContratCDDUData): Promise {
+ console.log('📄 [Contract Generation] Génération du PDF uniquement...');
+
+ const doc = ;
+ const pdfBlob = await ReactPDF.pdf(doc).toBlob();
+
+ const arrayBuffer = await pdfBlob.arrayBuffer();
+ const pdfBuffer = Buffer.from(arrayBuffer);
+
+ console.log(`✅ [Contract Generation] PDF généré (${pdfBuffer.byteLength} bytes)`);
+
+ return pdfBuffer;
+}
diff --git a/lib/pdf/index.ts b/lib/pdf/index.ts
new file mode 100644
index 0000000..19a79c1
--- /dev/null
+++ b/lib/pdf/index.ts
@@ -0,0 +1,42 @@
+/**
+ * Module de génération de PDFs avec @react-pdf/renderer
+ *
+ * Ce module remplace progressivement PDFMonkey pour la génération de PDFs.
+ *
+ * @example
+ * ```typescript
+ * import { generateAndUploadContract } from '@/lib/pdf';
+ *
+ * const result = await generateAndUploadContract(
+ * contractData,
+ * organizationId,
+ * contractId
+ * );
+ *
+ * // Enregistrer result.s3Key dans Supabase
+ * await supabase
+ * .from('contracts')
+ * .update({ pdf_url: result.s3Key })
+ * .eq('id', contractId);
+ * ```
+ */
+
+// Types
+export type { ContratCDDUData, CachetsData } from './types';
+export type { GenerateContractResult } from './generateContract';
+export type { UploadPdfOptions } from './uploadPdf';
+
+// Fonctions principales
+export {
+ generateAndUploadContract,
+ generateContractPdf
+} from './generateContract';
+
+export {
+ uploadPdfToS3,
+ generateContractS3Key,
+ generatePayslipS3Key
+} from './uploadPdf';
+
+// Composants (si besoin d'être utilisés directement)
+export { ContratCDDU } from './templates/ContratCDDU';
diff --git a/lib/pdf/templates/ContratCDDU.tsx b/lib/pdf/templates/ContratCDDU.tsx
new file mode 100644
index 0000000..d6b52a3
--- /dev/null
+++ b/lib/pdf/templates/ContratCDDU.tsx
@@ -0,0 +1,592 @@
+import React from 'react';
+import { Document, Page, Text, View, Image, StyleSheet, Font } from '@react-pdf/renderer';
+import { ContratCDDUData } from '../types';
+
+// Styles du document (conversion du CSS)
+const styles = StyleSheet.create({
+ page: {
+ fontFamily: 'Helvetica',
+ fontSize: 12,
+ paddingHorizontal: 50,
+ paddingVertical: 40,
+ },
+ logoContainer: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 20,
+ },
+ logo: {
+ width: 180,
+ },
+ title: {
+ textAlign: 'center',
+ marginBottom: 30,
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
+ bold: {
+ fontWeight: 'bold',
+ },
+ section: {
+ marginBottom: 30,
+ },
+ sectionObjet: {
+ marginBottom: 20,
+ },
+ sectionTitle: {
+ fontWeight: 'bold',
+ marginBottom: 10,
+ fontSize: 12,
+ },
+ paragraph: {
+ marginBottom: 0,
+ textAlign: 'justify',
+ },
+ list: {
+ marginLeft: 0,
+ },
+ listItem: {
+ marginBottom: 5,
+ },
+ infoLabel: {
+ fontWeight: 'bold',
+ marginBottom: 5,
+ },
+ infoValue: {
+ textAlign: 'left',
+ marginBottom: 5,
+ },
+ infoDelegation: {
+ textAlign: 'left',
+ fontStyle: 'italic',
+ marginBottom: 5,
+ },
+ signatureSpace: {
+ marginTop: 20,
+ marginBottom: 60,
+ },
+});
+
+interface ContratCDDUProps {
+ data: ContratCDDUData;
+}
+
+export const ContratCDDU: React.FC = ({ data }) => {
+ // Helpers pour la logique conditionnelle
+ const isMadame = data.employee_civ === 'Madame';
+ const isMonsieur = data.employee_civ === 'Monsieur';
+ const isArtiste = data.employee_catpro === 'Artiste';
+ const isTechnicien = data.employee_catpro === 'Technicien';
+ const isMetteurEnScene = data.employee_catpro === 'Metteur en scène';
+
+ // Titre du contrat selon la catégorie
+ const getTitreContrat = () => {
+ if (isArtiste) return 'ARTISTE';
+ if (isMadame && isTechnicien) return 'TECHNICIENNE';
+ if (isMonsieur && isTechnicien) return 'TECHNICIEN';
+ if (isMadame && isMetteurEnScene) return '\nARTISTE CADRE';
+ return 'ARTISTE CADRE';
+ };
+
+ // Formatage des dates travaillées
+ const getDatesFormatted = () => {
+ if (!data.dates_travaillees || data.dates_travaillees === '00') return null;
+ return data.dates_travaillees.split(';').map(d => d.trim());
+ };
+
+ // Manipulation du lieu de naissance (retirer "Le ")
+ const getCobFormatted = () => {
+ const cob = data.employee_cob;
+ if (cob.startsWith('Le ')) {
+ return { prefix: 'au', ville: cob.replace(/^Le /, '') };
+ }
+ return { prefix: 'à', ville: cob };
+ };
+
+ // Manipulation de la ville (pour signature)
+ const getVilleSignature = () => {
+ const ville = data.structure_ville;
+ if (ville.includes('Le ')) {
+ return { prefix: 'Au', ville: ville.replace(/^Le /, '') };
+ }
+ return { prefix: 'À', ville };
+ };
+
+ // Convention collective formatée
+ const getCCNFormatted = () => {
+ if (Array.isArray(data.CCN)) {
+ return data.CCN.join(', ');
+ }
+ return data.CCN;
+ };
+
+ const cobData = getCobFormatted();
+ const villeSignature = getVilleSignature();
+ const datesArray = getDatesFormatted();
+ const ccnFormatted = getCCNFormatted();
+
+ return (
+
+
+ {/* Logo */}
+ {data.imageUrl && (
+
+
+
+ )}
+
+ {/* Titre */}
+
+ CONTRAT D'ENGAGEMENT {getTitreContrat()}
+
+
+ {/* Entre les soussignés */}
+
+ Entre les {isMonsieur ? 'soussignés' : 'soussignées'} :
+
+
+
+ {data.structure_name}
+ {data.forme_juridique}
+ {data.structure_adresse}
+ {data.structure_cpville} {data.structure_ville}
+ SIRET : {data.structure_siret}
+ {data.structure_licence !== 'n/a' && (
+
+ Licence d'entrepreneur de spectacles : {data.structure_licence}
+
+ )}
+
+ représentée par {data.structure_signataire}, en sa qualité{' '}
+ {data.structure_signatairequalite === 'Administrateur' ? "d'" : 'de '}
+ {data.structure_signatairequalite}
+ {data.delegation === 'Oui'
+ ? ', pour le représentant légal et par délégation.'
+ : '.'}
+
+
+
+ d'une part,
+ et :
+
+ {/* Salarié */}
+
+
+ {data.employee_civ} {data.employee_firstname} {data.employee_lastname}
+ {data.employee_birthname !== data.employee_lastname && (
+ <>
+ {isMonsieur ? ', né ' : ', née '}
+ {data.employee_birthname}
+ >
+ )}
+ {data.employee_pseudo !== 'n/a' && (
+ <>
+ , {isMonsieur ? 'dit' : 'dite'} "{data.employee_pseudo}"
+ >
+ )}
+
+
+ {isMonsieur ? 'né' : 'née'} le {data.employee_dob} {cobData.prefix} {cobData.ville}
+
+ demeurant {data.employee_address}
+ {(!data.employee_ss || data.employee_ss === 0 || data.employee_ss === '') ? (
+
+ Le numéro de Sécurité Sociale du salarié est en cours d'attribution.
+
+ ) : (
+
+ N° de Sécurité Sociale : {data.employee_ss}
+
+ )}
+ N° Congés Spectacles : {data.employee_cs}
+
+ {/* Représentant légal si mineur */}
+ {data.mineur1618 === 'Oui' && (
+
+ dont {data.representant_civ === 'Monsieur' ? 'le représentant légal' : 'la représentante légale'} est{' '}
+ {data.representant_civ} {data.representant_nom},{' '}
+ {data.representant_civ === 'Monsieur' ? 'né' : 'née'} le {data.representant_dob} à{' '}
+ {data.representant_cob}, demeurant {data.representant_adresse}.
+
+ )}
+
+
+ d'autre part.
+
+ {/* Préambule */}
+
+ Le présent contrat est conclu dans le cadre de la législation du travail, des usages en vigueur dans la
+ profession, de l'article L. 1242-2° du Code du travail et de l'accord interbranche sur le recours au
+ contrat à durée déterminée d'usage dans le spectacle du 12/10/1998. Il est, en outre, régi par les
+ dispositions de la {ccnFormatted}
+ {ccnFormatted.includes('Convention Collective Nationale de l\'Édition') &&
+ ' et de ses annexes afférentes à l\'Édition Phonographique'}.
+
+
+ Il a été convenu et arrêté ce qui suit :
+
+ {/* Section OBJET */}
+
+ OBJET
+
+ {data.employee_civ} {data.employee_firstname} {data.employee_lastname} est{' '}
+ {isMonsieur ? 'engagé' : 'engagée'} selon l'objet suivant :
+
+
+ Profession : {data.employee_profession}
+ Code emploi : {data.employee_codeprofession}
+ {(data.structure_spectacle === 'Oui' && data.type_numobjet !== 'Administratif') ||
+ ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ||
+ ccnFormatted.includes('Convention Collective Nationale de l\'Édition') ? (
+
+
+ {data.structure_spectacle === 'Oui' && data.type_numobjet !== 'Administratif'
+ ? 'Spectacle'
+ : 'Production'}
+ : {data.spectacle}
+
+ ) : null}
+ {data.numobjet ? (
+
+ Numéro d'objet : {data.numobjet}
+
+ ) : (
+
+ Le numéro d'objet de cette production est en cours d'attribution.
+
+ )}
+
+
+
+ {/* Section DURÉE DE L'ENGAGEMENT - Partie 1 */}
+
+ DURÉE DE L'ENGAGEMENT
+
+ {data.date_debut === data.date_fin ? (
+ <>Le présent engagement couvre la journée du {data.date_debut}, pour >
+ ) : (
+ <>
+ {datesArray ? (
+ <>Le présent engagement couvre la période du {data.date_debut} au {data.date_fin} pour les dates travaillées suivantes :>
+ ) : (
+ <>Le présent engagement couvre la période du {data.date_debut} au {data.date_fin}.>
+ )}
+ >
+ )}
+
+
+ {/* Dates travaillées */}
+ {datesArray && (
+
+ {datesArray.map((date, index) => (
+
+ - {date}{index < datesArray.length - 1 ? ' ;' : ''}
+
+ ))}
+
+ )}
+
+ {/* Suite selon catégorie professionnelle */}
+ {data.date_debut !== data.date_fin && Pour }
+
+ {/* Artiste */}
+ {isArtiste && (
+
+ un total de{' '}
+ {data.cachets.representations >= 1 && data.cachets.repetitions >= 1 && (
+ <>
+ {data.cachets.representations} {data.cachets.representations === 1 ? 'cachet' : 'cachets'} de représentation et{' '}
+ {data.cachets.repetitions} {data.cachets.repetitions === 1 ? 'service' : 'services'} de répétition.
+ >
+ )}
+ {data.cachets.representations >= 1 && data.cachets.repetitions === 0 && (
+ <>
+ {data.cachets.representations} {data.cachets.representations === 1 ? 'cachet' : 'cachets'}
+ {ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ||
+ ccnFormatted.includes('Convention Collective Nationale de l\'Édition')
+ ? ' d\'enregistrement.'
+ : ' de représentation.'}
+ >
+ )}
+ {data.cachets.representations === 0 && data.cachets.repetitions >= 1 && (
+ <>
+ {data.cachets.repetitions} {data.cachets.repetitions === 1 ? 'service' : 'services'} de répétition.
+ >
+ )}
+
+ )}
+
+ {/* Technicien */}
+ {isTechnicien && (
+
+ un total de {data.cachets.heures} heures de travail
+ {data.cachets.heuresparjour === 0 ? '.' : `, à raison de ${data.cachets.heuresparjour} heures par jour de travail.`}
+
+ )}
+
+ {/* Metteur en scène */}
+ {isMetteurEnScene && (
+
+ {data.cachets.representations >= 1 && data.cachets.heures > 0 ? (
+ <>
+ un total de {data.cachets.representations} {data.cachets.representations === 1 ? 'cachet' : 'cachets'} de représentation et{' '}
+ {data.cachets.heures} heures de travail.
+ >
+ ) : data.cachets.representations === 0 ? (
+ <>un total de {data.cachets.heures} heures de travail.>
+ ) : (
+ <>un total de {data.cachets.representations} {data.cachets.representations === 1 ? 'cachet' : 'cachets'} de représentation.>
+ )}
+
+ )}
+
+ {/* Durée répétitions */}
+ {data.cachets.repetitions >= 1 && (
+
+ La durée totale des répétitions sera de {data.cachets.heures} heures
+ {data.cachets.heuresparjour === 0
+ ? '.'
+ : `, à raison de ${data.cachets.heuresparjour} heures par journée de répétition.`}
+
+ )}
+
+
+
+
+ Il ne nous sera, en aucun cas, fait obligation de proroger le présent engagement à expiration. La fin de la période d'engagement prévue
+ aux présentes, prorogée éventuellement de la durée de dépassement, en constitue le terme. Il n'y a lieu à aucun préavis.
+
+
+
+ {/* LIEUX D'ENGAGEMENT ET HORAIRES DE TRAVAIL */}
+
+ LIEUX D'ENGAGEMENT ET HORAIRES DE TRAVAIL
+
+ {data.structure_name} communiquera à {data.employee_firstname} {data.employee_lastname} les lieux{' '}
+ {ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ||
+ ccnFormatted.includes('Convention Collective Nationale de l\'Édition') ? (
+ <>>
+ ) : data.cachets.representations >= 1 && data.cachets.repetitions === 0 ? (
+ <>des représentations>
+ ) : data.cachets.representations === 0 && data.cachets.repetitions >= 1 ? (
+ <>des répétitions>
+ ) : isTechnicien ? (
+ <>d'engagement>
+ ) : data.cachets.representations >= 1 && data.cachets.repetitions >= 1 ? (
+ <>des répétitions et des représentations>
+ ) : null}
+ {isMetteurEnScene && data.cachets.representations === 0 && <>d'exercice de sa fonction>}
+ , ainsi que ses horaires de travail.
+
+
+
+ {/* RÉMUNÉRATION */}
+
+ RÉMUNÉRATION
+
+ Il sera alloué à {data.employee_firstname} {data.employee_lastname} à titre de salaire la somme de {data.salaire_brut} euros bruts.
+
+ {data.precisions_salaire && (
+
+ À titre informatif, la répartition de ce salaire brut est la suivante : {data.precisions_salaire}.
+
+ )}
+ {data.panierrepas && data.hebergement && (
+
+ {data.employee_firstname} {data.employee_lastname} percevra {data.panierrepas}{' '}
+ {data.panierrepas === '1' ? 'panier repas principal ' : 'paniers repas principaux, '}
+ et {data.hebergement} {data.hebergement === '1' ? 'indemnité' : 'indemnités'} d'hébergement et petit-déjeuner,{' '}
+ {data.panierrepasccn === 'Oui' && data.hebergementccn === 'Oui' ? (
+ <>selon les conditions prévues par la Convention Collective.>
+ ) : data.panierrepasccn === 'Non' && data.hebergementccn === 'Oui' ? (
+ <>
+ à hauteur de {data.montantpanierrepas} euros par panier repas principal, et selon les conditions prévues par la Convention Collective pour l'indemnité hébergement et petit-déjeuner.
+ >
+ ) : data.panierrepasccn === 'Oui' && data.hebergementccn === 'Non' ? (
+ <>
+ selon les conditions prévues par la Convention Collective pour les paniers repas principaux, et à hauteur de {data.montanthebergement} euros par indemnité hébergement et petit-déjeuner.
+ >
+ ) : (
+ <>
+ à hauteur de {data.montantpanierrepas} euros par panier repas principal et à hauteur de {data.montanthebergement} euros par indemnité hébergement et petit-déjeuner.
+ >
+ )}
+
+ )}
+ {data.autreprecision && (
+ {data.autreprecision}
+ )}
+
+
+ {/* RETRAITE ET CONGÉS PAYÉS */}
+
+ RETRAITE ET CONGÉS PAYÉS
+
+ Les cotisations de retraite seront versées à AUDIENS - 7 rue Jean Bleuzen - 92177 VANVES Cedex. L'employeur acquittera ses
+ contributions à la caisse des Congés Spectacles conformément à la législation et dans la limite des plafonds applicables en vigueur.
+
+
+
+ {/* ABSENCE-MALADIE */}
+
+ ABSENCE-MALADIE
+
+ En cas de maladie ou d'empêchement d'assurer{' '}
+ {isMetteurEnScene ? (
+ <>ses missions de mise en scène,>
+ ) : ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ? (
+ <>ses missions de {data.employee_profession},>
+ ) : ccnFormatted.includes('Convention Collective Nationale de l\'Édition') ? (
+ <>un enregistrement,>
+ ) : (
+ <>une répétition ou une représentation,>
+ )}{' '}
+ {data.employee_firstname} {data.employee_lastname} sera {isMonsieur ? 'tenu' : 'tenue'} d'en aviser {data.structure_name} dans un délai de 24 heures en précisant la durée probable de son absence. En cas de prolongation d'arrêt de travail,
+ {' '}{data.employee_firstname} {data.employee_lastname} devra transmettre à {data.structure_name}, dans les plus brefs délais, le certificat médical
+ justifiant de cette prolongation. En tout état de cause, les parties conviennent expressément qu'en cas de maladie de {data.employee_firstname} {data.employee_lastname},
+ le présent contrat pourra être résilié de plein droit par {data.structure_name} et ce, dans le respect des dispositions de la convention collective applicable.
+
+
+
+ {/* DROIT DE PRIORITÉ ET D'EXCLUSIVITÉ */}
+
+ DROIT DE PRIORITÉ ET D'EXCLUSIVITÉ
+
+ Le présent contrat donne à {data.structure_name} une priorité absolue sur tous les autres engagements que pourrait conclure par ailleurs {data.employee_firstname} {data.employee_lastname}, sur la période de l'engagement.
+ La dérogation éventuelle à cette clause devra faire l'objet d'un accord écrit de {data.structure_name}.
+
+
+ {data.employee_firstname} {data.employee_lastname} ne pourra en aucun cas refuser sa présence{' '}
+ {isMetteurEnScene ? (
+ <>sur ses lieux de travail et aux répétitions>
+ ) : ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ? (
+ <>sur les lieux de production>
+ ) : ccnFormatted.includes('Convention Collective Nationale de l\'Édition') ? (
+ <>sur les lieux d'enregistrement>
+ ) : (
+ <>à une répétition ou à une représentation>
+ )}{' '}
+ pour cause d'engagement extérieur, à quelque moment qu'il·elle ait été prévenu{' '}
+ {isMetteurEnScene ? (
+ <>de ses horaires et jours de travail et de l'existence de répétitons.>
+ ) : ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ? (
+ <>de ses horaires, jours et lieux de travail.>
+ ) : ccnFormatted.includes('Convention Collective Nationale de l\'Édition') ? (
+ <>de cet session d'enregistrement.>
+ ) : (
+ <>de l'existence de cette répétition ou représentation.>
+ )}
+
+
+
+ {/* MÉDECINE DU TRAVAIL */}
+
+ MÉDECINE DU TRAVAIL
+
+ {data.employee_firstname} {data.employee_lastname} déclare avoir satisfait aux obligations relatives à la Médecine du travail et communiquera
+ à {data.structure_name} l'attestation annuelle qui lui a été délivrée par cet organisme.
+
+
+
+ {/* ASSURANCES */}
+
+ ASSURANCES
+
+ {data.employee_firstname} {data.employee_lastname} est {isMonsieur ? 'tenu' : 'tenue'} d'assurer contre tous les risques tous les objets lui appartenant. {data.structure_name}
+ déclare avoir souscrit les assurances nécessaires à la couverture des risques liés
+ {ccnFormatted.includes('Convention Collective Nationale de la Production Audiovisuelle') ? (
+ <> à la production audiovisuelle.>
+ ) : ccnFormatted.includes('Convention Collective Nationale de l\'Édition') ? (
+ <> à l'édition phonographique.>
+ ) : (
+ <> aux représentations du spectacle.>
+ )}
+
+
+
+ {/* LITIGES */}
+
+ LITIGES
+
+ En cas de litige portant sur l'interprétation ou l'application du présent contrat, les parties conviennent de s'en remettre à l'appréciation des
+ tribunaux compétents, mais seulement après épuisement des voies amiables (conciliation, arbitrage).
+
+
+
+ {/* PROTECTION DES DONNÉES PERSONNELLES */}
+
+ PROTECTION DES DONNÉES PERSONNELLES
+
+ Aux fins de gestion du personnel et de traitement des rémunérations, nous sommes amenés à solliciter des données personnelles vous concernant
+ à l'occasion de la conclusion, l'exécution et le cas échéant, la rupture de votre contrat de travail.
+
+
+ La signature du présent contrat vaut autorisation pour la société de collecter, d'enregistrer et de stocker les données nécessaires.
+
+
+ Outre les services internes de {data.structure_name}, les destinataires de ces données sont, à ce jour, les organismes de sécurité sociale,
+ les caisses de retraite et de prévoyance, la mutuelle, France Travail Spectacle, les services des impôts, le service de médecine du travail, les organismes conventionnels et la société
+ Odentas Media SAS, notre prestataire de gestion de la paie.
+
+
+ Ces informations sont réservées à l'usage des services concernés et ne peuvent être communiquées qu'à ces destinataires.
+
+
+ Vous bénéficiez notamment d'un droit d'accès, de rectification et d'effacement des informations vous concernant, que vous pouvez exercer
+ en adressant directement une demande au responsable de ces traitements : {data.nom_responsable_traitement}, {data.qualite_responsable_traitement}, {data.email_responsable_traitement}.
+
+
+
+ {/* Fait à / Date signature */}
+
+ Fait en double exemplaire,
+
+ {villeSignature.prefix} {villeSignature.ville}, le {data.date_signature}.
+
+
+
+ {/* Signatures */}
+
+
+
+ {isMonsieur ? 'Le salarié :' : 'La salariée :'}
+
+
+ {data.employee_civ} {data.employee_firstname} {data.employee_lastname}
+
+ (Signature électronique via DocuSeal)
+
+
+ {/* Signature représentant légal si mineur */}
+ {data.mineur1618 === 'Oui' && (
+
+
+ {data.representant_civ === 'Monsieur' ? 'Le représentant légal' : 'La représentante légale'}
+ {isMadame ? ' de la salariée :' : ' du salarié :'}
+
+
+ {data.representant_civ} {data.representant_nom}
+
+ (Signature électronique via DocuSeal)
+
+ )}
+
+ {/* Signature employeur */}
+
+ L'employeur:
+ Pour {data.structure_name},
+ {data.delegation === 'Oui' && (
+
+ Pour le représentant légal et par délégation,
+
+ )}
+ {data.structure_signataire},
+ {data.structure_signatairequalite}.
+ (Signature électronique via DocuSeal)
+
+
+
+
+ );
+};
diff --git a/lib/pdf/types.ts b/lib/pdf/types.ts
new file mode 100644
index 0000000..94c58f5
--- /dev/null
+++ b/lib/pdf/types.ts
@@ -0,0 +1,82 @@
+/**
+ * Types pour la génération de PDF de contrats CDDU
+ */
+
+export interface CachetsData {
+ representations: number;
+ repetitions: number;
+ heures: number;
+ heuresparjour: number;
+}
+
+export interface ContratCDDUData {
+ // Structure employeur
+ structure_name: string;
+ structure_adresse: string;
+ structure_cpville: string;
+ structure_ville: string;
+ structure_siret: string;
+ structure_licence: string;
+ structure_signataire: string;
+ structure_signatairequalite: string;
+ structure_spectacle: string;
+ delegation: string;
+ forme_juridique: string;
+
+ // Représentant légal (mineur)
+ mineur1618: string;
+ representant_civ: string;
+ representant_nom: string;
+ representant_dob: string;
+ representant_cob: string;
+ representant_adresse: string;
+
+ // Salarié
+ employee_civ: string;
+ employee_firstname: string;
+ employee_lastname: string;
+ employee_birthname: string;
+ employee_dob: string;
+ employee_cob: string;
+ employee_address: string;
+ employee_ss: number | string;
+ employee_cs: string;
+ employee_profession: string;
+ employee_codeprofession: string;
+ employee_catpro: string;
+ employee_pseudo: string;
+
+ // Spectacle/Production
+ spectacle: string;
+ numobjet: string;
+ type_numobjet: string;
+
+ // Dates et durée
+ date_debut: string;
+ date_fin: string;
+ dates_travaillees: string;
+ date_signature: string;
+
+ // Rémunération
+ salaire_brut: string;
+ precisions_salaire: string;
+ panierrepas: string;
+ panierrepasccn: string;
+ montantpanierrepas: string;
+ hebergement: string;
+ hebergementccn: string;
+ montanthebergement: string;
+ autreprecision: string;
+ cachets: CachetsData;
+
+ // Convention collective
+ CCN: string | string[];
+
+ // Protection des données
+ nom_responsable_traitement: string;
+ qualite_responsable_traitement: string;
+ email_responsable_traitement: string;
+
+ // Logo
+ imageUrl?: string;
+}
diff --git a/lib/pdf/uploadPdf.ts b/lib/pdf/uploadPdf.ts
new file mode 100644
index 0000000..435e2a7
--- /dev/null
+++ b/lib/pdf/uploadPdf.ts
@@ -0,0 +1,125 @@
+import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
+
+const REGION = process.env.AWS_REGION || 'eu-west-3';
+const BUCKET = (process.env.AWS_S3_BUCKET || 'odentas-docs').trim();
+
+const s3Client = new S3Client({
+ region: REGION,
+ credentials: {
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
+ },
+});
+
+export interface UploadPdfOptions {
+ /** Buffer du PDF à uploader */
+ pdfBuffer: Buffer;
+ /** Clé S3 (chemin + nom du fichier), ex: 'contrats/2025/contrat-123.pdf' */
+ key: string;
+ /** Type de contenu (par défaut: application/pdf) */
+ contentType?: string;
+ /** Métadonnées supplémentaires */
+ metadata?: Record;
+}
+
+/**
+ * Upload un PDF généré sur S3
+ *
+ * @param options - Options d'upload
+ * @returns La clé S3 du fichier uploadé
+ *
+ * @example
+ * ```typescript
+ * const pdfBuffer = await generateContratPdf(data);
+ * const s3Key = await uploadPdfToS3({
+ * pdfBuffer,
+ * key: `contrats/${organizationId}/${contractId}.pdf`,
+ * metadata: {
+ * contractId: contractId,
+ * organizationId: organizationId,
+ * generatedAt: new Date().toISOString(),
+ * }
+ * });
+ * ```
+ */
+export async function uploadPdfToS3(options: UploadPdfOptions): Promise {
+ const { pdfBuffer, key, contentType = 'application/pdf', metadata = {} } = options;
+
+ console.log('📤 [S3 Upload] Début de l\'upload du PDF:', {
+ key,
+ bucket: BUCKET,
+ region: REGION,
+ size: pdfBuffer.byteLength,
+ metadata,
+ });
+
+ try {
+ const command = new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: key,
+ Body: pdfBuffer,
+ ContentType: contentType,
+ Metadata: metadata,
+ });
+
+ await s3Client.send(command);
+
+ console.log('✅ [S3 Upload] PDF uploadé avec succès:', {
+ key,
+ bucket: BUCKET,
+ size: pdfBuffer.byteLength,
+ });
+
+ return key;
+ } catch (error) {
+ console.error('❌ [S3 Upload] Erreur lors de l\'upload du PDF:', error);
+ throw new Error(`Échec de l'upload du PDF sur S3: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+
+/**
+ * Génère une clé S3 pour un contrat CDDU
+ *
+ * @param organizationId - ID de l'organisation
+ * @param contractId - ID du contrat
+ * @param year - Année du contrat (par défaut: année courante)
+ * @returns Clé S3 formatée
+ *
+ * @example
+ * ```typescript
+ * const key = generateContractS3Key('org-123', 'contract-456', 2025);
+ * // Retourne: 'contrats/org-123/2025/contract-456.pdf'
+ * ```
+ */
+export function generateContractS3Key(
+ organizationId: string,
+ contractId: string,
+ year: number = new Date().getFullYear()
+): string {
+ return `contrats/${organizationId}/${year}/${contractId}.pdf`;
+}
+
+/**
+ * Génère une clé S3 pour une fiche de paie
+ *
+ * @param organizationId - ID de l'organisation
+ * @param payslipId - ID de la fiche de paie
+ * @param year - Année de la fiche de paie
+ * @param month - Mois de la fiche de paie (1-12)
+ * @returns Clé S3 formatée
+ *
+ * @example
+ * ```typescript
+ * const key = generatePayslipS3Key('org-123', 'payslip-456', 2025, 10);
+ * // Retourne: 'fiches-paie/org-123/2025/10/payslip-456.pdf'
+ * ```
+ */
+export function generatePayslipS3Key(
+ organizationId: string,
+ payslipId: string,
+ year: number,
+ month: number
+): string {
+ const monthPadded = String(month).padStart(2, '0');
+ return `fiches-paie/${organizationId}/${year}/${monthPadded}/${payslipId}.pdf`;
+}
diff --git a/package-lock.json b/package-lock.json
index 10719a8..388e69b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/default-layout": "^3.12.0",
+ "@react-pdf/renderer": "^4.3.1",
"@supabase/auth-helpers-nextjs": "^0.10.0",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.57.4",
@@ -3384,6 +3385,189 @@
"react-dom": ">=16.8.0"
}
},
+ "node_modules/@react-pdf/fns": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz",
+ "integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==",
+ "license": "MIT"
+ },
+ "node_modules/@react-pdf/font": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.3.tgz",
+ "integrity": "sha512-N1qQDZr6phXYQOp033Hvm2nkUkx2LkszjGPbmRavs9VOYzi4sp31MaccMKptL24ii6UhBh/z9yPUhnuNe/qHwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-pdf/pdfkit": "^4.0.4",
+ "@react-pdf/types": "^2.9.1",
+ "fontkit": "^2.0.2",
+ "is-url": "^1.2.4"
+ }
+ },
+ "node_modules/@react-pdf/image": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz",
+ "integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-pdf/png-js": "^3.0.0",
+ "jay-peg": "^1.1.1"
+ }
+ },
+ "node_modules/@react-pdf/layout": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.1.tgz",
+ "integrity": "sha512-GVzdlWoZWldRDzlWj3SttRXmVDxg7YfraAohwy+o9gb9hrbDJaaAV6jV3pc630Evd3K46OAzk8EFu8EgPDuVuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-pdf/fns": "3.1.2",
+ "@react-pdf/image": "^3.0.3",
+ "@react-pdf/primitives": "^4.1.1",
+ "@react-pdf/stylesheet": "^6.1.1",
+ "@react-pdf/textkit": "^6.0.0",
+ "@react-pdf/types": "^2.9.1",
+ "emoji-regex-xs": "^1.0.0",
+ "queue": "^6.0.1",
+ "yoga-layout": "^3.2.1"
+ }
+ },
+ "node_modules/@react-pdf/pdfkit": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.4.tgz",
+ "integrity": "sha512-/nITLggsPlB66bVLnm0X7MNdKQxXelLGZG6zB5acF5cCgkFwmXHnLNyxYOUD4GMOMg1HOPShXDKWrwk2ZeHsvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@react-pdf/png-js": "^3.0.0",
+ "browserify-zlib": "^0.2.0",
+ "crypto-js": "^4.2.0",
+ "fontkit": "^2.0.2",
+ "jay-peg": "^1.1.1",
+ "linebreak": "^1.1.0",
+ "vite-compatible-readable-stream": "^3.6.1"
+ }
+ },
+ "node_modules/@react-pdf/png-js": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz",
+ "integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==",
+ "license": "MIT",
+ "dependencies": {
+ "browserify-zlib": "^0.2.0"
+ }
+ },
+ "node_modules/@react-pdf/primitives": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz",
+ "integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==",
+ "license": "MIT"
+ },
+ "node_modules/@react-pdf/reconciler": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz",
+ "integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4.1.1",
+ "scheduler": "0.25.0-rc-603e6108-20241029"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-pdf/reconciler/node_modules/scheduler": {
+ "version": "0.25.0-rc-603e6108-20241029",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz",
+ "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==",
+ "license": "MIT"
+ },
+ "node_modules/@react-pdf/render": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.1.tgz",
+ "integrity": "sha512-v1WAaAhQShQZGcBxfjkEThGCHVH9CSuitrZ1bIOLvB5iBKM14abYK5D6djKhWCwF6FTzYeT2WRjRMVgze/ND2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@react-pdf/fns": "3.1.2",
+ "@react-pdf/primitives": "^4.1.1",
+ "@react-pdf/textkit": "^6.0.0",
+ "@react-pdf/types": "^2.9.1",
+ "abs-svg-path": "^0.1.1",
+ "color-string": "^1.9.1",
+ "normalize-svg-path": "^1.1.0",
+ "parse-svg-path": "^0.1.2",
+ "svg-arc-to-cubic-bezier": "^3.2.0"
+ }
+ },
+ "node_modules/@react-pdf/renderer": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.1.tgz",
+ "integrity": "sha512-dPKHiwGTaOsKqNWCHPYYrx8CDfAGsUnV4tvRsEu0VPGxuot1AOq/M+YgfN/Pb+MeXCTe2/lv6NvA8haUtj3tsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@react-pdf/fns": "3.1.2",
+ "@react-pdf/font": "^4.0.3",
+ "@react-pdf/layout": "^4.4.1",
+ "@react-pdf/pdfkit": "^4.0.4",
+ "@react-pdf/primitives": "^4.1.1",
+ "@react-pdf/reconciler": "^1.1.4",
+ "@react-pdf/render": "^4.3.1",
+ "@react-pdf/types": "^2.9.1",
+ "events": "^3.3.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "queue": "^6.0.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-pdf/renderer/node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/@react-pdf/stylesheet": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.1.tgz",
+ "integrity": "sha512-Iyw0A3wRIeQLN4EkaKf8yF9MvdMxiZ8JjoyzLzDHSxnKYoOA4UGu84veCb8dT9N8MxY5x7a0BUv/avTe586Plg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-pdf/fns": "3.1.2",
+ "@react-pdf/types": "^2.9.1",
+ "color-string": "^1.9.1",
+ "hsl-to-hex": "^1.0.0",
+ "media-engine": "^1.0.3",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "node_modules/@react-pdf/textkit": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz",
+ "integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-pdf/fns": "3.1.2",
+ "bidi-js": "^1.0.2",
+ "hyphen": "^1.6.4",
+ "unicode-properties": "^1.4.1"
+ }
+ },
+ "node_modules/@react-pdf/types": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.1.tgz",
+ "integrity": "sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-pdf/font": "^4.0.3",
+ "@react-pdf/primitives": "^4.1.1",
+ "@react-pdf/stylesheet": "^6.1.1"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -4995,6 +5179,12 @@
"optional": true,
"peer": true
},
+ "node_modules/abs-svg-path": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
+ "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
+ "license": "MIT"
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -5510,6 +5700,15 @@
"bcrypt": "bin/bcrypt"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -5553,6 +5752,30 @@
"node": ">=8"
}
},
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
+ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
+ "node_modules/browserify-zlib/node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/browserslist": {
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
@@ -5813,6 +6036,15 @@
"wrap-ansi": "^6.2.0"
}
},
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cloudinary": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.8.0.tgz",
@@ -5892,6 +6124,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -5974,6 +6216,12 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "license": "MIT"
+ },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
@@ -6174,6 +6422,12 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
+ "license": "MIT"
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -6273,6 +6527,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/emoji-regex-xs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
+ "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
+ "license": "MIT"
+ },
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@@ -6930,7 +7190,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -7107,6 +7366,32 @@
}
}
},
+ "node_modules/fontkit": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
+ "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.12",
+ "brotli": "^1.3.2",
+ "clone": "^2.1.2",
+ "dfa": "^1.2.0",
+ "fast-deep-equal": "^3.1.3",
+ "restructure": "^3.0.0",
+ "tiny-inflate": "^1.0.3",
+ "unicode-properties": "^1.4.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/fontkit/node_modules/@swc/helpers": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
+ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -7650,6 +7935,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/hsl-to-hex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz",
+ "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==",
+ "license": "MIT",
+ "dependencies": {
+ "hsl-to-rgb-for-reals": "^1.1.0"
+ }
+ },
+ "node_modules/hsl-to-rgb-for-reals": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
+ "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
+ "license": "ISC"
+ },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@@ -7678,6 +7978,12 @@
"node": ">= 6"
}
},
+ "node_modules/hyphen": {
+ "version": "1.10.6",
+ "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz",
+ "integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
+ "license": "ISC"
+ },
"node_modules/ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
@@ -7794,6 +8100,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -8160,6 +8472,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-url": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+ "license": "MIT"
+ },
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -8252,6 +8570,15 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/jay-peg": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
+ "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==",
+ "license": "MIT",
+ "dependencies": {
+ "restructure": "^3.0.0"
+ }
+ },
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -8481,6 +8808,25 @@
"url": "https://github.com/sponsors/antonk52"
}
},
+ "node_modules/linebreak": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "0.0.8",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/linebreak/node_modules/base64-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -8642,6 +8988,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/media-engine": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
+ "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
+ "license": "MIT"
+ },
"node_modules/merge-refs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
@@ -9022,6 +9374,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/normalize-svg-path": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
+ "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
+ "license": "MIT",
+ "dependencies": {
+ "svg-arc-to-cubic-bezier": "^3.0.0"
+ }
+ },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
@@ -9047,7 +9408,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9295,6 +9655,12 @@
"node": ">=6"
}
},
+ "node_modules/parse-svg-path": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
+ "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -9753,7 +10119,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/posthog-js": {
@@ -9817,7 +10182,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -9878,6 +10242,15 @@
"node": ">=0.4.x"
}
},
+ "node_modules/queue": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
+ "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "~2.0.3"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -9954,7 +10327,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/react-pdf": {
@@ -10166,6 +10538,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -10213,6 +10594,12 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/restructure": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
+ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
+ "license": "MIT"
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -10624,6 +11011,15 @@
"optional": true,
"peer": true
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
@@ -10697,8 +11093,6 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
@@ -11039,6 +11433,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-arc-to-cubic-bezier": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
+ "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
+ "license": "ISC"
+ },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
@@ -11207,6 +11607,12 @@
"node": ">=0.8"
}
},
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+ "license": "MIT"
+ },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -11476,6 +11882,32 @@
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"license": "MIT"
},
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
+ "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "license": "MIT"
+ },
"node_modules/unrs-resolver": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
@@ -11640,7 +12072,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/utrie": {
@@ -11661,6 +12092,20 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/vite-compatible-readable-stream": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
+ "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@@ -12032,6 +12477,12 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yoga-layout": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
+ "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
+ "license": "MIT"
}
}
}
diff --git a/package.json b/package.json
index e959cde..b7dea89 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/default-layout": "^3.12.0",
+ "@react-pdf/renderer": "^4.3.1",
"@supabase/auth-helpers-nextjs": "^0.10.0",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.57.4",