feat(naa): Amélioration UX modal EditNAA - replier/déplier

- Tous les clients repliés par défaut à l'ouverture du modal
- Boutons 'Tout replier' / 'Tout déplier' pour gérer tous les clients
- Section factures repliable avec bouton Afficher/Masquer
- Affichage résumé facture sélectionnée quand section repliée
- Nouveau client déplié automatiquement pour faciliter la saisie
- Améliore la lisibilité pour NAA avec nombreux clients
This commit is contained in:
odentas 2025-10-31 15:28:44 +01:00
parent 35d5283434
commit 6485db4a75
18 changed files with 3290 additions and 51 deletions

286
PDF/CDDU.html Normal file
View file

@ -0,0 +1,286 @@
<div style="display: flex; justify-content: center;">
<img src="{{imageUrl}}" style="width: 180px;" />
</div>
{% assign cachet_representation = "cachet_representation" %}
<h1>CONTRAT D'ENGAGEMENT {% if employee_catpro == "Artiste" %}ARTISTE{% elsif employee_civ == "Madame" and employee_catpro == "Technicien" %}TECHNICIENNE
{% elsif employee_civ == "Monsieur" and employee_catpro == "Technicien" %}TECHNICIEN{% elsif employee_civ == "Madame" and employee_catpro == "Metteur·se en scène" %}<br>ARTISTE CADRE{% else %}ARTISTE CADRE{% endif %}</h1>
<p class="bold">Entre les {% if employee_civ == "Monsieur" %}soussignés{% else %}soussignées{% endif %} :</p>
<ul>
<li class="bold">{{structure_name}}</li>
<li>{{forme_juridique}}</li>
<li>{{structure_adresse}}</li>
<li>{{structure_cpville}} {{structure_ville}}</li>
<li>SIRET : {{structure_siret}}</li>
{% if structure_licence != "n/a" %}<li>Licence d'entrepreneur de spectacles : {{structure_licence}}</li>{% endif %}
<li>représentée par {{structure_signataire}}, en sa qualité {% if structure_signatairequalite == "Administrateur" %}d'{% else %}de {% endif %}{{structure_signatairequalite}}{% if delegation == "Oui" %}, pour le représentant légal et par délégation.{% else %}.{% endif %}</li>
</ul>
<p class="bold">d'une part,</p>
<p class="bold">et :</p>
<ul>
<li class="bold">{{employee_civ}} {{employee_firstname}} {{employee_lastname}}{% if employee_birthname != employee_lastname %}{% if employee_civ == "Monsieur" %}, né {{employee_birthname}}{% elsif employee_civ == "Madame" %}, née {{employee_birthname}}{% endif %}{% endif %}{% if employee_pseudo != "n/a" %}, {% if employee_civ == "Monsieur" %}dit{% else %}dite{% endif %} "{{employee_pseudo}}"{% endif %}</li>
{% assign cob_sans_le = employee_cob | remove_first: "Le " %}
<li>
{% if employee_civ == "Monsieur" %}
{% else %}
née
{% endif %}
le {{ employee_dob }}
{% if employee_cob contains "Le " %}
au {{ cob_sans_le }}
{% else %}
à {{ employee_cob }}
{% endif %}
</li>
<li>demeurant {{employee_address}}</li>
{% if employee_ss == 0 or employee_ss == '' or employee_ss == nil %}
<li>Le numéro de Sécurité Sociale du salarié est en cours d'attribution.</li>
{% else %}
<li>N° de Sécurité Sociale : {{ employee_ss }}</li>
{% endif %}
<li>N° Congés Spectacles : {{employee_cs}}</li>
{% if mineur1618 == "Oui" %}<li>dont {% if representant_civ == "Monsieur" %}le représentant légal{% elsif representant_civ == "Madame" %}la représentante légale{% endif %} est {{representant_civ}} {{representant_nom}}, {% if representant_civ == "Monsieur" %}né{% elsif representant_civ == "Madame" %}née{% endif %} le {{representant_dob}} à {{representant_cob}}, demeurant {{representant_adresse}}.</li>{% endif %}
</ul>
<p class="bold">d'autre part.</p>
<p>
Le présent contrat est conclu dans le cadre de la législation du travail, des usages en vigueur dans la
profession, de larticle L. 1242-2° du Code du travail et de laccord interbranche sur le recours au
contrat à durée déterminée dusage dans le spectacle du 12/10/1998. Il est, en outre, régi par les
dispositions de la {{ CCN | join: ', ' }}{% if CCN contains "Convention Collective Nationale de l'Édition" %} et de ses annexes afférentes à l'Édition Phonographique{% endif %}.
</p>
<p>
Il a été convenu et arrêté ce qui suit :
</p>
<div class="section">
<h2 class="section-title">OBJET</h2>
{{employee_civ}} {{employee_firstname}} {{employee_lastname}} est {% if employee_civ == "Monsieur" %}engagé{% else %}engagée{% endif %} selon l'objet suivant :
<ul>
<li><b>Profession</b> : {{employee_profession}}</li>
<li><b>Code emploi</b> : {{employee_codeprofession}}</li>
<li>
{% if structure_spectacle == "Oui" and type_numobjet != "Administratif" %}
<b>Spectacle</b> : {{ spectacle }}
{% elsif CCN contains "Convention Collective Nationale de la Production Audiovisuelle" or CCN contains "Convention Collective Nationale de l'Édition" %}
<b>Production</b> : {{ spectacle }}
{% endif %}
</li>
<li>{% if numobjet != empty %}<b>Numéro d'objet</b> : {{numobjet}}
{% elsif numobjet == empty %}Le <b>numéro d'objet</b> de cette production est en cours d'attribution.
{% endif %}</li>
</ul>
</div>
<div class="section-objet">
<h2 class="section-title">DURÉE DE L'ENGAGEMENT</h2>
<p>
{% if date_debut == date_fin %}
Le présent engagement couvre la journée du {{ date_debut }}, pour
{% else %}
{% if dates_travaillees != empty %}
Le présent engagement couvre la période du {{ date_debut }} au {{ date_fin }} pour les dates travaillées suivantes :
{% else %}
Le présent engagement couvre la période du {{ date_debut }} au {{ date_fin }}.
{% endif %}
<ul>
{% assign dates = dates_travaillees | split: ";" %}
{% for date in dates %}
<li>
- {{ date | strip }}
{% if forloop.last %}
{% else %}
;
{% endif %}
</li>
{% endfor %}
</ul>
Pour {% endif %}
{% if employee_catpro == "Artiste" %}
un total de {% if cachets.representations >= 1 and cachets.repetitions >= 1 %} {{cachets.representations}} {% if cachets.representations == 1 %}cachet{% else %}cachets{% endif %} de représentation et {{cachets.repetitions}} {% if cachets.repetitions == 1 %}service{% else %}services{% endif %} de répétition.{% endif %}
{% if cachets.representations >= 1 and cachets.repetitions == 0 %} {{cachets.representations}} {% if cachets.representations == 1 %}cachet{% else %}cachets{% endif %}{% if CCN contains "Convention Collective Nationale de la Production Audiovisuelle" or CCN contains "Convention Collective Nationale de l'Édition" %} d'enregistrement.{% endif %}{% unless CCN contains "Convention Collective Nationale de la Production Audiovisuelle"
or CCN contains "Convention Collective Nationale de l'Édition" %}
de représentation.{% endunless %}{% endif %}
{% if cachets.representations == 0 and cachets.repetitions >= 1 %}
{{cachets.repetitions}} {% if cachets.repetitions == 1 %}service{% else %}services{% endif %} de répétition.
{% endif %}
{% endif %}
{% if employee_catpro == "Technicien" %}
un total de {{cachets.heures}} heures de travail{% if cachets.heuresparjour == 0 %}.{% endif %}{% if cachets.heuresparjour >= 1 %}, à raison de {{cachets.heuresparjour}} heures par jour de travail.{% endif %}
{% endif %}
{% if employee_catpro == "Metteur en scène" and cachets.representations >= 1 and cachets.heures > 0 %}
un total de {{cachets.representations}} {% if cachets.representations == 1 %}cachet{% else %}cachets{% endif %} de représentation et {{cachets.heures}} heures de travail.
{% endif %}
{% if employee_catpro == "Metteur en scène" and cachets.representations == 0 %}
un total de {{cachets.heures}} heures de travail.
{% endif %}
{% if employee_catpro == "Metteur en scène" and cachets.representations >= 1 and cachets.heures == 0 %}
un total de {{cachets.representations}} {% if cachets.representations == 1 %}cachet{% else %}cachets{% endif %} de représentation.
{% endif %}
</p>
<p>
{% if cachets.repetitions >= 1 %}La durée totale des répétitions sera de {{cachets.heures}} heures{% endif %}{% if cachets.repetitions >= 1 and cachets.heuresparjour == 0 %}.{% endif %}{% if cachets.repetitions >= 1 and cachets.heuresparjour >= 1 %}, à raison de {{cachets.heuresparjour}} heures par journée de répétition.{% endif %}
</p>
</div>
<div class="section">
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.
</div>
<div class="section">
<h2 class="section-title">LIEUX D'ENGAGEMENT ET HORAIRES DE TRAVAIL</h2>
<p>
{{structure_name}} communiquera à {{employee_firstname}} {{employee_lastname}} les lieux {% if CCN contains "Convention Collective Nationale de la Production Audiovisuelle" or CCN contains "Convention Collective Nationale de l'Édition" %}{% elsif cachets.representations >=1 and cachets.repetitions == 0 %}des représentations{% elsif cachets.representations == 0 and cachets.repetitions >= 1 %}des répétitions{% elsif employee_catpro == "Technicien" %}d'engagement{% elsif cachets.representations >= 1 and cachets.repetitions >= 1 %}des répétitions et des représentations{% endif %}{% if employee_catpro == "Metteur en scène" and cachets.representations == 0 %}d'exercice de sa fonction{% endif %}, ainsi que ses horaires de travail.
</p>
</div>
<div class="section">
<h2 class="section-title">RÉMUNÉRATION</h2>
<p>
Il sera alloué à {{employee_firstname}} {{employee_lastname}} à titre de salaire la somme de {{salaire_brut}} euros bruts.
</p>
{% if precisions_salaire != blank %}<p>
À titre informatif, la répartition de ce salaire brut est la suivante : {{precisions_salaire}}.
</p>{% endif %}
{% if panierrepas != blank and hebergement != blank %}
<p>{{employee_firstname}} {{employee_lastname}} percevra {{panierrepas}}
{% if panierrepas == "1" %}panier repas principal {% else %}paniers repas principaux, {% endif %}et {{hebergement}} {% if hebergement == "1" %}indemnité{% else %}indemnités{% endif %} d'hébergement et petit-déjeuner,
{% if panierrepasccn == "Oui" and hebergementccn == "Oui" %} selon les conditions prévues par la Convention Collective.
{% elsif panierrepasccn == "Non" and hebergementccn == "Oui" %} à hauteur de {{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.
{% elsif panierrepasccn == "Oui" and hebergementccn == "Non" %} selon les conditions prévues par la Convention Collective pour les paniers repas principaux, et à hauteur de {{montanthebergement}} euros par indemnité hébergement et petit-déjeuner.
{% elsif panierrepasccn == "Non" and hebergementccn == "Non" %} à hauteur de {{montantpanierrepas}} euros par panier repas principal et à hauteur de {{montanthebergement}} euros par indemnité hébergement et petit-déjeuner.
{% endif %}</p>
{% endif %}
{% if autreprecision != blank %}<p>{{autreprecision}}</p>{% endif %}
</div>
<div class="section">
<h2 class="section-title">RETRAITE ET CONGÉS PAYÉS</h2>
<p>
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.
</p>
</div>
<div class="section">
<h2 class="section-title">ABSENCE-MALADIE</h2>
<p>
En cas de maladie ou d'empêchement d'assurer {% if employee_catpro == "Metteur en scène" %}ses missions de mise en scène,{% elsif CCN contains "Convention Collective Nationale de la Production Audiovisuelle" %}ses missions de {{employee_profession}},{% elsif CCN contains "Convention Collective Nationale de l'Édition" %}un enregistrement,{% else %}une répétition ou une représentation,{% endif %} {{employee_firstname}} {{employee_lastname}} sera {% if employee_civ == "Monsieur" %}tenu{% else %}tenue{% endif %}
d'en aviser {{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,
{{employee_firstname}} {{employee_lastname}} devra transmettre à {{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 {{employee_firstname}} {{employee_lastname}},
le présent contrat pourra être résilié de plein droit par {{structure_name}} et ce, dans le respect des dispositions de la convention collective applicable.
</p>
</div>
<div class="section">
<h2 class="section-title">DROIT DE PRIORITÉ ET D'EXCLUSIVITÉ</h2>
<p>
Le présent contrat donne à {{structure_name}} une priorité absolue sur tous les autres engagements que pourrait conclure par ailleurs {{employee_firstname}} {{employee_lastname}}, sur la période de l'engagement.
La dérogation éventuelle à cette clause devra faire l'objet d'un accord écrit de {{structure_name}}.
</p>
<p>
{{employee_firstname}} {{employee_lastname}} ne pourra en aucun cas refuser sa présence {% if employee_catpro == "Metteur en scène" %}sur ses lieux de travail et aux répétitions{% elsif CCN contains "Convention Collective Nationale de la Production Audiovisuelle" %}sur les lieux de production{% elsif CCN contains "Convention Collective Nationale de l'Édition" %}sur les lieux d'enregistrement{% else %} à une répétition ou à une représentation{% endif %} pour cause
d'engagement extérieur, à quelque moment qu'il·elle ait été prévenu {% if employee_catpro == "Metteur en scène" %}de ses horaires et jours de travail et de l'existence de répétitons.{% elsif CCN contains "Convention Collective Nationale de la Production Audiovisuelle" %}de ses horaires, jours et lieux de travail.{% elsif CCN contains "Convention Collective Nationale de l'Édition" %}de cet session d'enregistrement.{% else %}de l'existence de cette répétition ou représentation.{% endif %}
</p>
</div>
<div class="section">
<h2 class="section-title">MÉDECINE DU TRAVAIL</h2>
<p>
{{employee_firstname}} {{employee_lastname}} déclare avoir satisfait aux obligations relatives à la Médecine du travail et communiquera
à {{structure_name}} l'attestation annuelle qui lui a été délivrée par cet organisme.
</p>
</div>
<div class="section">
<h2 class="section-title">ASSURANCES</h2>
<p>
{{employee_firstname}} {{employee_lastname}} est {% if employee_civ == "Monsieur" %}tenu{% else %}tenue{% endif %} d'assurer contre tous les risques tous les objets lui appartenant. {{structure_name}}
déclare avoir souscrit les assurances nécessaires à la couverture des risques liés{% if CCN contains "Convention Collective Nationale de la Production Audiovisuelle" %} à la production audiovisuelle.{% elsif CCN contains "Convention Collective Nationale de l'Édition" %} à l'édition phonographique.{% else %} aux représentations du spectacle.{% endif %}
</p>
</div>
<div class="section">
<h2 class="section-title">LITIGES</h2>
<p>
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).
</p>
</div>
<div class="section">
<h2 class="section-title">PROTECTION DES DONNÉES PERSONNELLES</h2>
<p>
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.
</p>
<p>
La signature du présent contrat vaut autorisation pour la société de collecter, d'enregistrer et de stocker les données nécessaires.
</p>
<p>
Outre les services internes de {{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.
</p>
<p>
Ces informations sont réservées à l'usage des services concernés et ne peuvent être communiquées qu'à ces destinataires.
</p>
<p>
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 : {{nom_responsable_traitement}}, {{qualite_responsable_traitement}}, {{email_responsable_traitement}}.
</p>
</div>
<div class="section">
<p>
Fait en double exemplaire,
</p>
{% assign ville_sans_le = structure_ville | remove_first: "Le " %}
<p>
{% if structure_ville contains "Le " %}
Au {{ ville_sans_le }}, le {{ date_signature }}.
{% else %}
À {{ structure_ville }}, le {{ date_signature }}.
{% endif %}
</p>
</p>
</div>
<div class="info-paragraph">
<div class="info-row">
<div class="info-label">{% if employee_civ == "Monsieur" %}Le salarié :{% else %}La salariée :{% endif %}</div>
<div class="info-value">{{employee_civ}} {{employee_firstname}} {{employee_lastname}}</div>
<br>
<div>{% raw %}{{Signature Employé;role=Salarié;type=signature;height=60;width=150}}{% endraw %}</div>
<br><br><br><br>
</div>
{% if mineur1618 == "Oui" %}
<div class="info-paragraph">
<div class="info-row">
<div class="info-label">{% if representant_civ == "Monsieur" %}Le représentant légal{% elsif representant_civ == "Madame" %}La représentante légale{% endif %}{% if employee_civ == "Madame" %} de la salariée :{% elsif employee_civ == "Monsieur" %} du salarié :{% endif %}</div>
<div class="info-value">{{representant_civ}} {{representant_nom}}</div>
<div class="info-signature">[[s|1]]</div>
<br><br>
</div>
{% endif %}
<div class="info-row">
<div class="info-label">L'employeur:</div>
<div class="info-value">Pour {{structure_name}},</div>
{% if delegation == "Oui" %}<div class="info-delegation">Pour le représentant légal et par délégation,</div>{% endif %}
<div class="info-value">{{structure_signataire}},</div>
<div class="info-value">{{structure_signatairequalite}}.</div>
<br>
<div>{% raw %}{{Signature Employeur;role=Employeur;type=signature;height=60;width=150}}{% endraw %}</div>
</div>
</div>

70
PDF/CSS_PDF.css Normal file
View file

@ -0,0 +1,70 @@
body {
font-family: Verdana, sans-serif;
font-size: 12px;
padding: 0 50px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
p {
margin-bottom: 0;
text-align: justify;
}
.bold {
font-weight: bold;
}
.section {
margin-bottom: 30px;
page-break-inside: avoid;
}
.section-objet {
margin-bottom: 20px;
}
.section_protection {
margin-bottom: 30px;
}
.section-title {
font-weight: bold;
margin-bottom: 10px;
}
ul {
list-style-type: none;
}
.info-paragraph {
display: flex;
flex-direction: column;
}
.info-signature {
font-size: 80px;
color: white;
}
.info-row {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.info-label {
font-weight: bold;
}
.info-value {
text-align: left;
}
.info-delegation {
text-align: left;
font-style: italic;
}

60
PDF/donnees_test.json Normal file
View file

@ -0,0 +1,60 @@
{
"structure_name": "Association Compagnie Lazara",
"structure_adresse": "1495 Chemin de Fenestrelle",
"structure_cpville": "13400",
"structure_ville": "Le Kremlin-Bicêtre",
"structure_siret": "824 938 161 00026",
"structure_licence": "2022-999999",
"structure_signataire": "Renaud BREVIERE-ABRAHAM",
"structure_signatairequalite": "Administrateur",
"structure_spectacle": "Non",
"forme_juridique": "Association",
"delegation": "Oui",
"mineur1618": "Non",
"representant_civ": "Monsieur",
"representant_nom": "Jacques DELRUE",
"representant_dob": "27/01/1989",
"representant_cob": "Nantes (44)",
"representant_adresse": "15 rue du Château, 44000 Nantes",
"employee_civ": "Monsieur",
"employee_firstname": "Jean",
"employee_lastname": "GOLTIER",
"employee_birthname": "BALTAZAR",
"employee_dob": "06/07/1992",
"employee_cob": "Le Kremlin-Bicêtre",
"employee_address": "48 Boulevard André Aune, 13006 MARSEILLE",
"employee_ss": 0,
"employee_cs": "F574430",
"employee_profession": "Batteur",
"employee_codeprofession": "BAT010",
"employee_catpro": "Metteur en scène",
"employee_pseudo": "n/a",
"spectacle": "Mamés",
"numobjet": "6Z000000000",
"type_numobjet": "Spectacle",
"date_debut": "01/07/2023",
"date_fin": "28/08/2023",
"dates_travaillees": "00",
"details_cachets": "",
"salaire_brut": "2764,80",
"date_signature": "01/07/2023",
"CCN": "Convention Collective Nationale de l'Édition",
"precisions_salaire": "",
"panierrepas": "",
"panierrepasccn": "Non",
"montantpanierrepas": "",
"hebergement": "",
"hebergementccn": "Non",
"montanthebergement": "",
"autreprecision": "Il est entendu entre GADMER Sylvie et Balumina Films SAS que toutes les éventuelles futures embauches de la salariée dans le cadre de la production de 'L'Europe s'est arrêtée à Istanbul', en tant que cheffe monteuse, se feront sur la base d'un salaire brut de 1500,00 euros par semaine de 39 heures travaillées.",
"nom_responsable_traitement": "Renaud BREVIERE-ABRAHAM",
"qualite_responsable_traitement": "Administrateur",
"email_responsable_traitement": "contact@compagnie-lazara.fr",
"cachets": {
"representations": 2,
"repetitions": 10,
"heures": 0,
"heuresparjour": 0
},
"imageUrl": ""
}

View file

@ -2,8 +2,9 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Plus, FileText, Download, Eye, Calendar, RefreshCw, Trash2 } from "lucide-react";
import { Plus, FileText, Download, Eye, Calendar, RefreshCw, Trash2, Edit } from "lucide-react";
import CreateNAAModal from "@/components/staff/CreateNAAModal";
import EditNAAModal from "@/components/staff/EditNAAModal";
type NAADocument = {
id: string;
@ -38,6 +39,7 @@ function formatCurrency(amount: number) {
export default function NAAPage() {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingNaaId, setEditingNaaId] = useState<string | null>(null);
const [loadingPdf, setLoadingPdf] = useState<string | null>(null);
const [regenerating, setRegenerating] = useState<string | null>(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() {
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => 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"
>
<Edit className="w-4 h-4" />
</button>
{naa.pdf_url && (
<>
<button
@ -333,6 +348,14 @@ export default function NAAPage() {
onSuccess={handleCreateSuccess}
/>
)}
{editingNaaId && (
<EditNAAModal
naaId={editingNaaId}
onClose={() => setEditingNaaId(null)}
onSuccess={handleEditSuccess}
/>
)}
</main>
);
}

View file

@ -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 }
);
}
}

View file

@ -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<string, any> = {};
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);

View file

@ -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 }
);
}
}

View file

@ -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

View file

@ -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 }
);
}
}

View file

@ -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 = <ContratCDDU data={data} />;
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 }
);
}
}

View file

@ -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<string | null>(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<ClientPrestations[]>([]);
// Factures détectées par organisation
const [clientInvoices, setClientInvoices] = useState<Record<string, Invoice[]>>({});
// Ensemble des invoices sélectionnées (IDs)
const [selectedInvoiceIds, setSelectedInvoiceIds] = useState<Record<string, string[]>>({});
// État d'expansion des sections factures par org_id
const [invoicesExpanded, setInvoicesExpanded] = useState<Record<string, boolean>>({});
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<ReferredClient[]>({
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 (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center gap-3">
<Loader2 className="w-6 h-6 animate-spin text-indigo-600" />
<span className="text-slate-700">Chargement de la NAA...</span>
</div>
</div>
);
}
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="px-6 py-4 border-b flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-slate-800">
Modifier la NAA
</h2>
<p className="text-sm text-slate-500 mt-1">
{naaData?.naa_number} - {naaData?.periode}
</p>
</div>
<button
onClick={onClose}
disabled={isUpdating}
className="text-slate-400 hover:text-slate-600 transition"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto">
<div className="p-6 space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl">
{error}
</div>
)}
{/* Info: Champs de base non modifiables */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<p className="text-sm text-blue-800">
<strong>Note :</strong> Les champs de base (apporteur, période, dates) ne sont pas modifiables.
Vous pouvez uniquement ajouter ou supprimer des prestations.
</p>
</div>
{/* Champs de base (lecture seule) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Apporteur d'affaires
</label>
<input
type="text"
value={`${referrerCode} - ${naaData?.referrer_name || ""}`}
disabled
className="w-full px-3 py-2 bg-slate-100 border rounded-lg text-slate-500 cursor-not-allowed"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Période
</label>
<input
type="text"
value={periode}
disabled
className="w-full px-3 py-2 bg-slate-100 border rounded-lg text-slate-500 cursor-not-allowed"
/>
</div>
</div>
{/* Prestations par client */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">
Prestations
</h3>
<div className="flex items-center gap-2">
{clientsPrestations.length > 0 && (
<>
<button
type="button"
onClick={collapseAll}
disabled={isUpdating}
className="px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg transition disabled:opacity-50"
>
Tout replier
</button>
<button
type="button"
onClick={expandAll}
disabled={isUpdating}
className="px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg transition disabled:opacity-50"
>
Tout déplier
</button>
</>
)}
<button
type="button"
onClick={addClient}
disabled={isUpdating}
className="px-4 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition disabled:opacity-50 flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Ajouter un client
</button>
</div>
</div>
{clientsPrestations.length === 0 ? (
<div className="text-center py-8 bg-slate-50 rounded-xl border border-dashed">
<p className="text-slate-500">Aucune prestation. Cliquez sur "Ajouter un client" pour commencer.</p>
</div>
) : (
<div className="space-y-3">
{clientsPrestations.map((client, clientIndex) => (
<div key={clientIndex} className="border rounded-xl overflow-hidden bg-white">
{/* En-tête du client */}
<div className="bg-slate-50 px-4 py-3 flex items-center justify-between border-b">
<button
type="button"
onClick={() => toggleClientExpanded(clientIndex)}
className="flex items-center gap-2 flex-1 text-left font-medium text-slate-700 hover:text-indigo-600 transition"
>
{client.expanded ? (
<ChevronDown className="w-5 h-5" />
) : (
<ChevronRight className="w-5 h-5" />
)}
<span>
{client.client || "Client non défini"} {client.code && `(${client.code})`}
</span>
<span className="ml-auto text-sm text-slate-500">
{client.lines.length} prestation{client.lines.length > 1 ? "s" : ""} Total: {getClientTotal(client).toFixed(2)}
</span>
</button>
<button
type="button"
onClick={() => removeClient(clientIndex)}
disabled={isUpdating}
className="ml-2 p-2 text-red-600 hover:bg-red-50 rounded-lg transition disabled:opacity-50"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{/* Contenu du client */}
{client.expanded && (
<div className="p-4 space-y-4">
{/* Sélection du client */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Nom du client
</label>
<select
value={client.client}
onChange={(e) => updateClient(clientIndex, e.target.value)}
disabled={isUpdating}
className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
>
<option value="">Sélectionner un client</option>
{referredClients.map((rc: any) => (
<option key={rc.org_id} value={rc.organizations.name}>
{rc.organizations.name} ({rc.code_employeur})
</option>
))}
</select>
</div>
{/* Lignes de prestations */}
{/* Factures détectées pour inclusion/exclusion */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-slate-700">Factures détectées</label>
{(() => {
const orgId = client.code ? referredClients.find(rc => rc.code_employeur === client.code)?.org_id : undefined;
const invoices = orgId ? clientInvoices[orgId] || [] : [];
if (orgId && invoices.length > 0) {
return (
<button
type="button"
onClick={() => toggleInvoicesExpanded(orgId)}
className="text-xs text-slate-500 hover:text-slate-700 flex items-center gap-1"
>
{invoicesExpanded[orgId] ? (
<>
<ChevronDown className="w-3 h-3" />
Masquer
</>
) : (
<>
<ChevronRight className="w-3 h-3" />
Afficher
</>
)}
</button>
);
}
return null;
})()}
</div>
<div className="bg-white border rounded-lg p-2">
{(() => {
const orgId = client.code ? referredClients.find(rc => rc.code_employeur === client.code)?.org_id : undefined;
const invoices = orgId ? clientInvoices[orgId] || [] : [];
if (!orgId) return <div className="text-sm text-slate-500">Sélectionnez un client pour voir les factures.</div>;
if (invoices.length === 0) return <div className="text-sm text-slate-500">Aucune facture détectée pour cette période.</div>;
// Si replié, afficher juste la facture sélectionnée
if (!invoicesExpanded[orgId]) {
const selectedId = (selectedInvoiceIds[orgId] || [])[0];
const selectedInvoice = invoices.find(inv => inv.id === selectedId);
if (selectedInvoice) {
return (
<div className="text-sm text-slate-700">
Facture sélectionnée : {selectedInvoice.period_label || new Date(selectedInvoice.created_at).toLocaleDateString("fr-FR")} {parseFloat(selectedInvoice.amount_ht).toFixed(2)}
</div>
);
}
return <div className="text-sm text-slate-500">Aucune facture sélectionnée</div>;
}
return (
<div className="space-y-2">
{invoices.map(inv => (
<label key={inv.id} className="flex items-center gap-3 text-sm cursor-pointer hover:bg-slate-50 p-2 rounded-lg transition">
<input
type="radio"
name={`invoice-${orgId}`}
checked={(selectedInvoiceIds[inv.org_id] || []).includes(inv.id)}
onChange={() => toggleInvoiceSelection(inv.org_id, inv.id)}
className="w-4 h-4"
/>
<span className="flex-1">{inv.period_label || new Date(inv.created_at).toLocaleDateString("fr-FR")} {parseFloat(inv.amount_ht).toFixed(2)} </span>
<span className="text-slate-400 text-xs">#{inv.id.substring(0, 8)}</span>
</label>
))}
</div>
);
})()}
</div>
</div>
<div className="space-y-3">
{client.lines.map((line, lineIndex) => (
<div key={lineIndex} className="grid grid-cols-12 gap-2 items-start p-3 bg-slate-50 rounded-lg">
<div className="col-span-4">
<label className="block text-xs font-medium text-slate-600 mb-1">
Type de prestation
</label>
<select
value={line.type_prestation}
onChange={(e) => updateLine(clientIndex, lineIndex, "type_prestation", e.target.value)}
disabled={isUpdating}
className="w-full px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
>
<option value="">Type</option>
{typesPrestation.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<div className="col-span-2">
<label className="block text-xs font-medium text-slate-600 mb-1">
Quantité
</label>
<input
type="number"
value={line.quantite}
onChange={(e) => updateLine(clientIndex, lineIndex, "quantite", e.target.value)}
disabled={isUpdating}
className="w-full px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
min="0"
step="1"
/>
</div>
<div className="col-span-2">
<label className="block text-xs font-medium text-slate-600 mb-1">
Tarif ()
</label>
<input
type="number"
value={line.tarif}
onChange={(e) => updateLine(clientIndex, lineIndex, "tarif", e.target.value)}
disabled={isUpdating}
className="w-full px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
min="0"
step="0.01"
/>
</div>
<div className="col-span-3">
<label className="block text-xs font-medium text-slate-600 mb-1">
Total ()
</label>
<input
type="number"
value={line.total.toFixed(2)}
readOnly
className="w-full px-2 py-1.5 text-sm border rounded-lg bg-slate-100 text-slate-600"
/>
</div>
<div className="col-span-1 flex items-end justify-center">
<button
type="button"
onClick={() => removeLine(clientIndex, lineIndex)}
disabled={isUpdating || client.lines.length === 1}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed"
title={client.lines.length === 1 ? "Impossible de supprimer la dernière prestation" : "Supprimer cette prestation"}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
{/* Bouton ajouter prestation */}
<button
type="button"
onClick={() => addLineToClient(clientIndex)}
disabled={isUpdating}
className="w-full py-2 text-sm text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50 rounded-lg transition flex items-center justify-center gap-1 disabled:opacity-50"
>
<Plus className="w-4 h-4" />
Ajouter une prestation pour ce client
</button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t flex items-center justify-end gap-3">
<button
type="button"
onClick={onClose}
disabled={isUpdating}
className="px-4 py-2 text-slate-700 hover:bg-slate-100 rounded-xl transition disabled:opacity-50"
>
Annuler
</button>
<button
type="submit"
disabled={isUpdating}
className="px-6 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isUpdating && <Loader2 className="w-4 h-4 animate-spin" />}
{isUpdating ? "Mise à jour en cours..." : "Mettre à jour et régénérer le PDF"}
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -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<GenerateContractResult> {
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 = <ContratCDDU data={data} />;
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<Buffer> {
console.log('📄 [Contract Generation] Génération du PDF uniquement...');
const doc = <ContratCDDU data={data} />;
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;
}

42
lib/pdf/index.ts Normal file
View file

@ -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';

View file

@ -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<ContratCDDUProps> = ({ 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 (
<Document>
<Page size="A4" style={styles.page}>
{/* Logo */}
{data.imageUrl && (
<View style={styles.logoContainer}>
<Image src={data.imageUrl} style={styles.logo} />
</View>
)}
{/* Titre */}
<Text style={styles.title}>
CONTRAT D'ENGAGEMENT {getTitreContrat()}
</Text>
{/* Entre les soussignés */}
<Text style={[styles.paragraph, styles.bold]}>
Entre les {isMonsieur ? 'soussignés' : 'soussignées'} :
</Text>
<View style={styles.list}>
<Text style={[styles.listItem, styles.bold]}>{data.structure_name}</Text>
<Text style={styles.listItem}>{data.forme_juridique}</Text>
<Text style={styles.listItem}>{data.structure_adresse}</Text>
<Text style={styles.listItem}>{data.structure_cpville} {data.structure_ville}</Text>
<Text style={styles.listItem}>SIRET : {data.structure_siret}</Text>
{data.structure_licence !== 'n/a' && (
<Text style={styles.listItem}>
Licence d'entrepreneur de spectacles : {data.structure_licence}
</Text>
)}
<Text style={styles.listItem}>
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.'
: '.'}
</Text>
</View>
<Text style={[styles.paragraph, styles.bold]}>d'une part,</Text>
<Text style={[styles.paragraph, styles.bold]}>et :</Text>
{/* Salarié */}
<View style={styles.list}>
<Text style={[styles.listItem, styles.bold]}>
{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}"
</>
)}
</Text>
<Text style={styles.listItem}>
{isMonsieur ? 'né' : 'née'} le {data.employee_dob} {cobData.prefix} {cobData.ville}
</Text>
<Text style={styles.listItem}>demeurant {data.employee_address}</Text>
{(!data.employee_ss || data.employee_ss === 0 || data.employee_ss === '') ? (
<Text style={styles.listItem}>
Le numéro de Sécurité Sociale du salarié est en cours d'attribution.
</Text>
) : (
<Text style={styles.listItem}>
N° de Sécurité Sociale : {data.employee_ss}
</Text>
)}
<Text style={styles.listItem}>N° Congés Spectacles : {data.employee_cs}</Text>
{/* Représentant légal si mineur */}
{data.mineur1618 === 'Oui' && (
<Text style={styles.listItem}>
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}.
</Text>
)}
</View>
<Text style={[styles.paragraph, styles.bold]}>d'autre part.</Text>
{/* Préambule */}
<Text style={styles.paragraph}>
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'}.
</Text>
<Text style={styles.paragraph}>Il a é convenu et arrêté ce qui suit :</Text>
{/* Section OBJET */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>OBJET</Text>
<Text style={styles.paragraph}>
{data.employee_civ} {data.employee_firstname} {data.employee_lastname} est{' '}
{isMonsieur ? 'engagé' : 'engagée'} selon l'objet suivant :
</Text>
<View style={styles.list}>
<Text style={styles.listItem}><Text style={styles.bold}>Profession</Text> : {data.employee_profession}</Text>
<Text style={styles.listItem}><Text style={styles.bold}>Code emploi</Text> : {data.employee_codeprofession}</Text>
{(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') ? (
<Text style={styles.listItem}>
<Text style={styles.bold}>
{data.structure_spectacle === 'Oui' && data.type_numobjet !== 'Administratif'
? 'Spectacle'
: 'Production'}
</Text> : {data.spectacle}
</Text>
) : null}
{data.numobjet ? (
<Text style={styles.listItem}>
<Text style={styles.bold}>Numéro d'objet</Text> : {data.numobjet}
</Text>
) : (
<Text style={styles.listItem}>
Le <Text style={styles.bold}>numéro d'objet</Text> de cette production est en cours d'attribution.
</Text>
)}
</View>
</View>
{/* Section DURÉE DE L'ENGAGEMENT - Partie 1 */}
<View style={styles.sectionObjet}>
<Text style={styles.sectionTitle}>DURÉE DE L'ENGAGEMENT</Text>
<Text style={styles.paragraph}>
{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}.</>
)}
</>
)}
</Text>
{/* Dates travaillées */}
{datesArray && (
<View style={styles.list}>
{datesArray.map((date, index) => (
<Text key={index} style={styles.listItem}>
- {date}{index < datesArray.length - 1 ? ' ;' : ''}
</Text>
))}
</View>
)}
{/* Suite selon catégorie professionnelle */}
{data.date_debut !== data.date_fin && <Text style={styles.paragraph}>Pour </Text>}
{/* Artiste */}
{isArtiste && (
<Text style={styles.paragraph}>
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.
</>
)}
</Text>
)}
{/* Technicien */}
{isTechnicien && (
<Text style={styles.paragraph}>
un total de {data.cachets.heures} heures de travail
{data.cachets.heuresparjour === 0 ? '.' : `, à raison de ${data.cachets.heuresparjour} heures par jour de travail.`}
</Text>
)}
{/* Metteur en scène */}
{isMetteurEnScene && (
<Text style={styles.paragraph}>
{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.</>
)}
</Text>
)}
{/* Durée répétitions */}
{data.cachets.repetitions >= 1 && (
<Text style={styles.paragraph}>
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.`}
</Text>
)}
</View>
<View style={styles.section}>
<Text style={styles.paragraph}>
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.
</Text>
</View>
{/* LIEUX D'ENGAGEMENT ET HORAIRES DE TRAVAIL */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>LIEUX D'ENGAGEMENT ET HORAIRES DE TRAVAIL</Text>
<Text style={styles.paragraph}>
{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.
</Text>
</View>
{/* RÉMUNÉRATION */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>RÉMUNÉRATION</Text>
<Text style={styles.paragraph}>
Il sera alloué à {data.employee_firstname} {data.employee_lastname} à titre de salaire la somme de {data.salaire_brut} euros bruts.
</Text>
{data.precisions_salaire && (
<Text style={styles.paragraph}>
À titre informatif, la répartition de ce salaire brut est la suivante : {data.precisions_salaire}.
</Text>
)}
{data.panierrepas && data.hebergement && (
<Text style={styles.paragraph}>
{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.
</>
)}
</Text>
)}
{data.autreprecision && (
<Text style={styles.paragraph}>{data.autreprecision}</Text>
)}
</View>
{/* RETRAITE ET CONGÉS PAYÉS */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>RETRAITE ET CONGÉS PAYÉS</Text>
<Text style={styles.paragraph}>
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.
</Text>
</View>
{/* ABSENCE-MALADIE */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>ABSENCE-MALADIE</Text>
<Text style={styles.paragraph}>
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.
</Text>
</View>
{/* DROIT DE PRIORITÉ ET D'EXCLUSIVITÉ */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>DROIT DE PRIORITÉ ET D'EXCLUSIVITÉ</Text>
<Text style={styles.paragraph}>
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}.
</Text>
<Text style={styles.paragraph}>
{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 é 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.</>
)}
</Text>
</View>
{/* MÉDECINE DU TRAVAIL */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>MÉDECINE DU TRAVAIL</Text>
<Text style={styles.paragraph}>
{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 é délivrée par cet organisme.
</Text>
</View>
{/* ASSURANCES */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>ASSURANCES</Text>
<Text style={styles.paragraph}>
{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.</>
)}
</Text>
</View>
{/* LITIGES */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>LITIGES</Text>
<Text style={styles.paragraph}>
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).
</Text>
</View>
{/* PROTECTION DES DONNÉES PERSONNELLES */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>PROTECTION DES DONNÉES PERSONNELLES</Text>
<Text style={styles.paragraph}>
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.
</Text>
<Text style={styles.paragraph}>
La signature du présent contrat vaut autorisation pour la société de collecter, d'enregistrer et de stocker les données nécessaires.
</Text>
<Text style={styles.paragraph}>
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.
</Text>
<Text style={styles.paragraph}>
Ces informations sont réservées à l'usage des services concernés et ne peuvent être communiquées qu'à ces destinataires.
</Text>
<Text style={styles.paragraph}>
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}.
</Text>
</View>
{/* Fait à / Date signature */}
<View style={styles.section}>
<Text style={styles.paragraph}>Fait en double exemplaire,</Text>
<Text style={styles.paragraph}>
{villeSignature.prefix} {villeSignature.ville}, le {data.date_signature}.
</Text>
</View>
{/* Signatures */}
<View style={styles.section}>
<View style={styles.signatureSpace}>
<Text style={styles.infoLabel}>
{isMonsieur ? 'Le salarié :' : 'La salariée :'}
</Text>
<Text style={styles.infoValue}>
{data.employee_civ} {data.employee_firstname} {data.employee_lastname}
</Text>
<Text style={styles.paragraph}>(Signature électronique via DocuSeal)</Text>
</View>
{/* Signature représentant légal si mineur */}
{data.mineur1618 === 'Oui' && (
<View style={styles.signatureSpace}>
<Text style={styles.infoLabel}>
{data.representant_civ === 'Monsieur' ? 'Le représentant légal' : 'La représentante légale'}
{isMadame ? ' de la salariée :' : ' du salarié :'}
</Text>
<Text style={styles.infoValue}>
{data.representant_civ} {data.representant_nom}
</Text>
<Text style={styles.paragraph}>(Signature électronique via DocuSeal)</Text>
</View>
)}
{/* Signature employeur */}
<View style={styles.signatureSpace}>
<Text style={styles.infoLabel}>L'employeur:</Text>
<Text style={styles.infoValue}>Pour {data.structure_name},</Text>
{data.delegation === 'Oui' && (
<Text style={styles.infoDelegation}>
Pour le représentant légal et par délégation,
</Text>
)}
<Text style={styles.infoValue}>{data.structure_signataire},</Text>
<Text style={styles.infoValue}>{data.structure_signatairequalite}.</Text>
<Text style={styles.paragraph}>(Signature électronique via DocuSeal)</Text>
</View>
</View>
</Page>
</Document>
);
};

82
lib/pdf/types.ts Normal file
View file

@ -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;
}

125
lib/pdf/uploadPdf.ts Normal file
View file

@ -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<string, string>;
}
/**
* 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<string> {
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`;
}

467
package-lock.json generated
View file

@ -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"
}
}
}

View file

@ -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",