feat: Préparation migration PDFMonkey vers Gotenberg

- Ajout helpers Handlebars pour remplacer filtres Liquid
- Conversion template CDDU de Liquid vers Handlebars
- Nouvelle API route /api/generate-contract-pdf pour Gotenberg
- Configuration Docker Compose pour auto-héberger Gotenberg
- Documentation complète de migration
- Variables d'environnement exemple

Note: Le bouton 'Créer PDF' utilise encore PDFMonkey.
Pour activer Gotenberg, modifier l'appel dans ContractEditor.tsx
This commit is contained in:
odentas 2025-12-27 21:40:27 +01:00
parent a12bf0ca0e
commit c6faceb038
6 changed files with 1129 additions and 0 deletions

8
.env.gotenberg.example Normal file
View file

@ -0,0 +1,8 @@
# Variables d'environnement pour Gotenberg
# Ajouter ces lignes dans .env.local
# URL de Gotenberg (local pour développement)
GOTENBERG_URL=http://localhost:3001
# En production (Railway, Coolify, VPS, etc.)
# GOTENBERG_URL=https://gotenberg.votre-domaine.com

View file

@ -0,0 +1,400 @@
# Migration PDFMonkey → Gotenberg
## Contexte
Ce guide explique comment migrer la génération de PDF des contrats de travail de **PDFMonkey** (SaaS avec templates Liquid) vers **Gotenberg** (auto-hébergé).
## Avantages de Gotenberg
- Auto-hébergé : contrôle total, pas de dépendance externe
- Gratuit et open-source
- Rapide et performant
- Supporte HTML/CSS nativement
- API simple et bien documentée
- Déployable sur Vercel, Railway, Coolify, etc.
## Architecture de la Solution
```
┌─────────────────┐
│ Next.js App │
│ (Formulaire) │
└────────┬────────┘
│ 1. Envoie données JSON
┌─────────────────────────┐
│ API Route Next.js │
│ /api/generate-contract │
│ │
│ - Compile template │
│ Handlebars │
│ - Génère HTML │
└────────┬────────────────┘
│ 2. Envoie HTML
┌─────────────────────────┐
│ Gotenberg Service │
│ (Docker/Auto-hébergé) │
│ │
│ - Convertit HTML → PDF │
│ - Retourne le PDF │
└────────┬────────────────┘
│ 3. Reçoit PDF
┌─────────────────────────┐
│ Supabase Storage │
│ (Stockage PDF) │
└─────────────────────────┘
```
## Fichiers Créés
### 1. Helpers Handlebars
**Fichier** : `lib/handlebars-helpers.ts`
Remplace les filtres Liquid par des helpers Handlebars :
- `removeFirst` : équivalent de `remove_first`
- `contains` : vérifier si une chaîne contient un pattern
- `split` : diviser une chaîne
- `eq`, `ne`, `gte`, `gt` : comparaisons
- `isEmpty`, `isNotEmpty` : vérifier les valeurs vides
- `includesAny` : vérifier plusieurs valeurs (pour les CCN)
### 2. Template Handlebars
**Fichier** : `templates-contrats/cddu-handlebars.html`
Template HTML/Handlebars qui remplace le template Liquid PDFMonkey.
**Principales conversions** :
| Liquid | Handlebars |
|--------|------------|
| `{% if condition %}` | `{{#if condition}}` |
| `{% elsif %}` | `{{else if}}` |
| `{% assign var = value %}` | Variables pré-calculées en JS |
| `{{ var \| filter }}` | `{{helper var}}` |
| `{% for item in array %}` | `{{#each array}}` |
### 3. API Route Gotenberg
**Fichier** : `app/api/generate-contract-pdf/route.ts`
Route API qui :
1. Authentifie l'utilisateur
2. Charge le template Handlebars
3. Compile le template avec les données
4. Envoie le HTML à Gotenberg
5. Reçoit le PDF
6. Upload sur Supabase Storage
7. Retourne l'URL du PDF
### 4. Docker Compose
**Fichier** : `docker-compose.gotenberg.yml`
Configuration Docker pour auto-héberger Gotenberg.
## Installation
### Étape 1 : Installer les dépendances
```bash
npm install handlebars
npm install --save-dev @types/handlebars
```
### Étape 2 : Configurer les variables d'environnement
Ajouter dans `.env.local` :
```bash
# URL de Gotenberg (local ou auto-hébergé)
GOTENBERG_URL=http://localhost:3001
# Ou en production (exemple avec Railway/Coolify)
# GOTENBERG_URL=https://gotenberg.votre-domaine.com
```
### Étape 3 : Déployer Gotenberg localement
```bash
docker-compose -f docker-compose.gotenberg.yml up -d
```
Vérifier que Gotenberg fonctionne :
```bash
curl http://localhost:3001/health
```
### Étape 4 : Tester l'API
Créer un fichier de test `test-generate-pdf.ts` :
```typescript
const response = await fetch('/api/generate-contract-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contractId: 'abc123',
contractType: 'cddu',
data: {
structure_name: 'Association Compagnie Lazara',
employee_firstname: 'Jean',
employee_lastname: 'GOLTIER',
// ... autres données
},
}),
});
const result = await response.json();
console.log('PDF généré:', result.pdfUrl);
```
## Déploiement en Production
### Option 1 : Railway (recommandé)
1. Créer un nouveau service sur Railway
2. Déployer l'image Docker : `gotenberg/gotenberg:8`
3. Exposer le port `3000`
4. Récupérer l'URL publique
5. Mettre à jour `GOTENBERG_URL` dans Vercel
### Option 2 : Coolify
1. Créer un nouveau service
2. Source : Docker Image
3. Image : `gotenberg/gotenberg:8`
4. Port : `3000`
5. Générer un domaine public
6. Mettre à jour `GOTENBERG_URL`
### Option 3 : VPS auto-hébergé
```bash
# Sur le VPS
git clone <votre-repo>
cd Projet\ Nouvel\ Espace\ Paie
docker-compose -f docker-compose.gotenberg.yml up -d
# Configurer Nginx reverse proxy
sudo nano /etc/nginx/sites-available/gotenberg
# Contenu :
server {
listen 80;
server_name gotenberg.votre-domaine.com;
location / {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Activer le site
sudo ln -s /etc/nginx/sites-available/gotenberg /etc/nginx/sites-enabled/
sudo systemctl reload nginx
```
## Intégration dans le Code Existant
### Modifier le formulaire de création de contrat
Dans `app/staff/contrats/[id]/page.tsx` ou le composant concerné :
```typescript
const handleGeneratePDF = async () => {
setIsGenerating(true);
try {
const response = await fetch('/api/generate-contract-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contractId: contract.id,
contractType: contract.regime === 'CDDU' ? 'cddu' : 'rg',
data: prepareContractData(contract),
}),
});
const result = await response.json();
if (result.success) {
toast.success('PDF généré avec succès');
// Ouvrir le PDF dans un nouvel onglet
window.open(result.pdfUrl, '_blank');
} else {
toast.error('Erreur lors de la génération du PDF');
}
} catch (error) {
console.error('Erreur:', error);
toast.error('Erreur lors de la génération du PDF');
} finally {
setIsGenerating(false);
}
};
// Fonction pour préparer les données
function prepareContractData(contract: Contract) {
return {
structure_name: contract.organization.name,
structure_adresse: contract.organization.address,
structure_cpville: contract.organization.postal_code,
structure_ville: contract.organization.city,
structure_siret: contract.organization.siret,
employee_firstname: contract.employee.first_name,
employee_lastname: contract.employee.last_name,
employee_dob: formatDate(contract.employee.birth_date),
// ... mapper toutes les données
imageUrl: contract.organization.logo_data_uri,
};
}
```
## Différences avec PDFMonkey
| Aspect | PDFMonkey | Gotenberg |
|--------|-----------|-----------|
| **Hébergement** | SaaS externe | Auto-hébergé |
| **Coût** | Payant (par document) | Gratuit |
| **Template** | Liquid (éditeur en ligne) | Handlebars (code local) |
| **Latence** | Variable (réseau externe) | Faible (interne) |
| **Contrôle** | Limité | Total |
| **Maintenance** | Aucune | Docker + monitoring |
## Gestion des Templates
### Modifier un template
1. Éditer `templates-contrats/cddu-handlebars.html`
2. Tester localement
3. Commit + push
4. Déploiement automatique via Vercel
### Créer un nouveau template
1. Dupliquer `cddu-handlebars.html`
2. Adapter le contenu
3. Ajouter le cas dans l'API route :
```typescript
case 'nouveau-type':
templatePath = path.join(process.cwd(), 'templates-contrats', 'nouveau-type.html');
break;
```
## Monitoring et Logs
### Vérifier les logs Gotenberg
```bash
docker logs odentas-gotenberg --tail 100 -f
```
### Health check
```bash
curl http://localhost:3001/health
```
### Métriques de performance
Ajouter dans l'API route :
```typescript
const startTime = Date.now();
// ... génération PDF
const duration = Date.now() - startTime;
console.log(`PDF généré en ${duration}ms`);
```
## Troubleshooting
### Erreur : Gotenberg inaccessible
**Symptôme** : `fetch failed` ou timeout
**Solutions** :
1. Vérifier que Gotenberg est démarré : `docker ps`
2. Vérifier le health check : `curl http://localhost:3001/health`
3. Vérifier les logs : `docker logs odentas-gotenberg`
4. Vérifier la variable `GOTENBERG_URL`
### Erreur : Template non trouvé
**Symptôme** : `ENOENT: no such file or directory`
**Solutions** :
1. Vérifier le chemin du template
2. Vérifier que le template existe bien dans `templates-contrats/`
3. En production Vercel, vérifier que les templates sont inclus dans le build
### Erreur : Rendu incorrect du PDF
**Symptôme** : Mise en page cassée, polices manquantes
**Solutions** :
1. Vérifier le CSS dans le template
2. Utiliser des polices web-safe ou inclure les fonts en base64
3. Tester le HTML seul dans un navigateur
4. Ajuster les marges dans l'API route
### Performance lente
**Symptôme** : Génération de PDF > 5 secondes
**Solutions** :
1. Augmenter les ressources Docker
2. Optimiser les images (compression, taille)
3. Réduire la complexité du HTML/CSS
4. Utiliser un cache pour les templates compilés
## Checklist de Migration
- [ ] Installer les dépendances npm
- [ ] Créer les helpers Handlebars
- [ ] Convertir le template CDDU
- [ ] Créer l'API route
- [ ] Déployer Gotenberg localement
- [ ] Tester la génération de PDF en local
- [ ] Déployer Gotenberg en production
- [ ] Configurer la variable `GOTENBERG_URL` sur Vercel
- [ ] Intégrer dans le formulaire de contrat
- [ ] Tester en production
- [ ] Migrer les autres templates (RG, Avenants)
- [ ] Désactiver PDFMonkey
## Rollback
Si besoin de revenir à PDFMonkey :
1. Réactiver les appels à l'API PDFMonkey
2. Commenter les appels à `/api/generate-contract-pdf`
3. Garder le code Gotenberg en standby
## Next Steps
Une fois la migration terminée :
1. **Migrer les autres templates** : RG, Avenants, etc.
2. **Optimiser les performances** : cache de templates, parallélisation
3. **Ajouter des analytics** : temps de génération, taux de succès
4. **Backup automatique** : sauvegarder les PDFs générés
5. **Versioning des templates** : Git + tags pour suivre les changements
## Support
En cas de problème, consulter :
- [Documentation Gotenberg](https://gotenberg.dev/)
- [Documentation Handlebars](https://handlebarsjs.com/)
- Logs Docker : `docker logs odentas-gotenberg`
- Logs Next.js : Console Vercel
---
**Auteur** : Équipe Odentas
**Date** : Décembre 2025
**Version** : 1.0

View file

@ -0,0 +1,163 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import Handlebars from 'handlebars';
import fs from 'fs/promises';
import path from 'path';
import { registerHandlebarsHelpers } from '@/lib/handlebars-helpers';
registerHandlebarsHelpers();
/**
* API Route pour générer un contrat PDF via Gotenberg
* POST /api/generate-contract-pdf
*
* Body attendu :
* {
* contractId: string,
* contractType: "cddu" | "rg" | "avenant",
* data: object (données du contrat)
* }
*/
export async function POST(request: NextRequest) {
try {
const supabase = createRouteHandlerClient({ cookies });
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json(
{ error: 'Non authentifié' },
{ status: 401 }
);
}
const body = await request.json();
const { contractId, contractType, data } = body;
if (!contractId || !contractType || !data) {
return NextResponse.json(
{ error: 'Données manquantes (contractId, contractType, data)' },
{ status: 400 }
);
}
// Vérifier que l'utilisateur a accès à ce contrat
const { data: contract, error: contractError } = await supabase
.from('contracts')
.select('*')
.eq('id', contractId)
.single();
if (contractError || !contract) {
return NextResponse.json(
{ error: 'Contrat introuvable' },
{ status: 404 }
);
}
// Charger le template Handlebars selon le type de contrat
let templatePath: string;
switch (contractType) {
case 'cddu':
templatePath = path.join(process.cwd(), 'templates-contrats', 'cddu-handlebars.html');
break;
case 'rg':
templatePath = path.join(process.cwd(), 'templates-contrats', 'rg-handlebars.html');
break;
case 'avenant':
templatePath = path.join(process.cwd(), 'templates-contrats', 'avenant-handlebars.html');
break;
default:
return NextResponse.json(
{ error: 'Type de contrat invalide' },
{ status: 400 }
);
}
const templateContent = await fs.readFile(templatePath, 'utf-8');
const template = Handlebars.compile(templateContent);
// Générer le HTML avec les données
const html = template(data);
// Envoyer le HTML à Gotenberg pour conversion en PDF
const gotenbergUrl = process.env.GOTENBERG_URL || 'http://localhost:3001';
const formData = new FormData();
formData.append('files', new Blob([html], { type: 'text/html' }), 'index.html');
// Options Gotenberg
formData.append('marginTop', '1');
formData.append('marginBottom', '1');
formData.append('marginLeft', '1');
formData.append('marginRight', '1');
formData.append('paperWidth', '21');
formData.append('paperHeight', '29.7');
formData.append('preferCssPageSize', 'false');
const gotenbergResponse = await fetch(`${gotenbergUrl}/forms/chromium/convert/html`, {
method: 'POST',
body: formData,
});
if (!gotenbergResponse.ok) {
const errorText = await gotenbergResponse.text();
console.error('Erreur Gotenberg:', errorText);
return NextResponse.json(
{ error: 'Erreur lors de la génération du PDF', details: errorText },
{ status: 500 }
);
}
// Récupérer le PDF généré
const pdfBuffer = await gotenbergResponse.arrayBuffer();
const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
// Uploader le PDF sur Supabase Storage
const fileName = `contract_${contractId}_${Date.now()}.pdf`;
const { data: uploadData, error: uploadError } = await supabase.storage
.from('contracts')
.upload(fileName, pdfBlob, {
contentType: 'application/pdf',
upsert: false,
});
if (uploadError) {
console.error('Erreur upload Supabase:', uploadError);
return NextResponse.json(
{ error: 'Erreur lors de l\'upload du PDF' },
{ status: 500 }
);
}
// Mettre à jour le contrat avec l'URL du PDF
const { data: publicUrl } = supabase.storage
.from('contracts')
.getPublicUrl(fileName);
const { error: updateError } = await supabase
.from('contracts')
.update({ pdf_url: publicUrl.publicUrl })
.eq('id', contractId);
if (updateError) {
console.error('Erreur mise à jour contrat:', updateError);
}
return NextResponse.json({
success: true,
pdfUrl: publicUrl.publicUrl,
fileName,
});
} catch (error) {
console.error('Erreur génération PDF:', error);
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,54 @@
version: '3.8'
services:
gotenberg:
image: gotenberg/gotenberg:8
container_name: odentas-gotenberg
restart: unless-stopped
ports:
- "3001:3000"
environment:
# Configuration de sécurité
- GOTENBERG_API_TIMEOUT=30s
- GOTENBERG_API_ROOT_PATH=/
# Limites de ressources pour éviter la surcharge
- GOTENBERG_CHROMIUM_MAX_QUEUE_SIZE=10
- GOTENBERG_CHROMIUM_AUTO_START=true
# Options de conversion PDF
- GOTENBERG_CHROMIUM_DISABLE_JAVASCRIPT=false
- GOTENBERG_CHROMIUM_ALLOW_LIST=^file:///tmp/.*
# Limites de ressources Docker
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
# Health check pour vérifier que Gotenberg est opérationnel
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Volumes pour logs et cache (optionnel)
volumes:
- gotenberg-cache:/tmp/gotenberg
networks:
- odentas-network
volumes:
gotenberg-cache:
driver: local
networks:
odentas-network:
driver: bridge

114
lib/handlebars-helpers.ts Normal file
View file

@ -0,0 +1,114 @@
/**
* Helpers Handlebars personnalisés pour remplacer les filtres Liquid
* Utilisés pour la génération de contrats PDF avec Gotenberg
*/
import Handlebars from 'handlebars';
/**
* Enregistre tous les helpers Handlebars personnalisés
*/
export function registerHandlebarsHelpers() {
// Helper pour remplacer "remove_first" de Liquid
Handlebars.registerHelper('removeFirst', function(str: string, pattern: string) {
if (!str) return '';
return str.replace(pattern, '');
});
// Helper pour remplacer "contains" de Liquid
Handlebars.registerHelper('contains', function(str: string, pattern: string) {
if (!str) return false;
return str.includes(pattern);
});
// Helper pour remplacer "strip" de Liquid (trim)
Handlebars.registerHelper('strip', function(str: string) {
if (!str) return '';
return str.trim();
});
// Helper pour remplacer "join" de Liquid
Handlebars.registerHelper('join', function(array: any[], separator: string) {
if (!Array.isArray(array)) return '';
return array.join(separator);
});
// Helper pour split (diviser une chaîne)
Handlebars.registerHelper('split', function(str: string, delimiter: string) {
if (!str) return [];
return str.split(delimiter);
});
// Helper pour vérifier l'égalité
Handlebars.registerHelper('eq', function(a: any, b: any) {
return a === b;
});
// Helper pour vérifier l'inégalité
Handlebars.registerHelper('ne', function(a: any, b: any) {
return a !== b;
});
// Helper pour vérifier >=
Handlebars.registerHelper('gte', function(a: number, b: number) {
return a >= b;
});
// Helper pour vérifier >
Handlebars.registerHelper('gt', function(a: number, b: number) {
return a > b;
});
// Helper pour vérifier ==
Handlebars.registerHelper('lte', function(a: number, b: number) {
return a <= b;
});
// Helper pour vérifier <
Handlebars.registerHelper('lt', function(a: number, b: number) {
return a < b;
});
// Helper pour vérifier si une valeur est vide
Handlebars.registerHelper('isEmpty', function(value: any) {
return !value || value === '' || value === 'n/a' || value === 0;
});
// Helper pour vérifier si une valeur n'est pas vide
Handlebars.registerHelper('isNotEmpty', function(value: any) {
return value && value !== '' && value !== 'n/a' && value !== 0;
});
// Helper OR logique
Handlebars.registerHelper('or', function(...args: any[]) {
// Enlever le dernier argument qui est l'objet options de Handlebars
const values = args.slice(0, -1);
return values.some(v => !!v);
});
// Helper AND logique
Handlebars.registerHelper('and', function(...args: any[]) {
// Enlever le dernier argument qui est l'objet options de Handlebars
const values = args.slice(0, -1);
return values.every(v => !!v);
});
// Helper pour formatter un nombre avec virgules
Handlebars.registerHelper('formatNumber', function(value: number) {
if (value === null || value === undefined) return '';
return value.toString().replace('.', ',');
});
// Helper raw pour éviter l'échappement HTML
Handlebars.registerHelper('raw', function(options) {
return options.fn();
});
// Helper pour comparer avec plusieurs valeurs (utile pour les CCN)
Handlebars.registerHelper('includesAny', function(str: string, ...values: any[]) {
if (!str) return false;
// Enlever le dernier argument (options de Handlebars)
const searchValues = values.slice(0, -1);
return searchValues.some(v => str.includes(v));
});
}

View file

@ -0,0 +1,390 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
font-size: 12pt;
line-height: 1.5;
margin: 40px;
}
h1 {
text-align: center;
font-size: 16pt;
margin-bottom: 20px;
}
h2.section-title {
font-size: 13pt;
margin-top: 20px;
margin-bottom: 10px;
}
.bold {
font-weight: bold;
}
.section {
margin-bottom: 15px;
}
.section-objet {
margin-bottom: 15px;
}
ul {
list-style-type: none;
padding-left: 0;
}
li {
margin-bottom: 5px;
}
.info-paragraph {
margin-top: 30px;
}
.info-row {
margin-bottom: 20px;
}
.info-label {
font-weight: bold;
margin-bottom: 5px;
}
.info-value {
margin-bottom: 3px;
}
.info-delegation {
font-style: italic;
margin-bottom: 3px;
}
</style>
</head>
<body>
<div style="display: flex; justify-content: center;">
<img src="{{imageUrl}}" style="width: 180px;" />
</div>
<h1>CONTRAT D'ENGAGEMENT
{{#eq employee_catpro "Artiste"}}ARTISTE{{/eq}}
{{#if (and (eq employee_civ "Madame") (eq employee_catpro "Technicien"))}}TECHNICIENNE{{/if}}
{{#if (and (eq employee_civ "Monsieur") (eq employee_catpro "Technicien"))}}TECHNICIEN{{/if}}
{{#if (and (eq employee_civ "Madame") (eq employee_catpro "Metteur en scène"))}}<br>ARTISTE CADRE{{/if}}
{{#if (and (eq employee_civ "Monsieur") (eq employee_catpro "Metteur en scène"))}}ARTISTE CADRE{{/if}}
</h1>
<p class="bold">Entre les {{#eq employee_civ "Monsieur"}}soussignés{{else}}soussignées{{/eq}} :</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>
{{#ne structure_licence "n/a"}}<li>Licence d'entrepreneur de spectacles : {{structure_licence}}</li>{{/ne}}
<li>représentée par {{structure_signataire}}, en sa qualité {{#eq structure_signatairequalite "Administrateur"}}d'{{else}}de {{/eq}}{{structure_signatairequalite}}{{#eq delegation "Oui"}}, pour le représentant légal et par délégation.{{else}}.{{/eq}}</li>
</ul>
<p class="bold">d'une part,</p>
<p class="bold">et :</p>
<ul>
<li class="bold">{{employee_civ}} {{employee_firstname}} {{employee_lastname}}{{#ne employee_birthname employee_lastname}}{{#eq employee_civ "Monsieur"}}, né {{employee_birthname}}{{/eq}}{{#eq employee_civ "Madame"}}, née {{employee_birthname}}{{/eq}}{{/ne}}{{#ne employee_pseudo "n/a"}}, {{#eq employee_civ "Monsieur"}}dit{{else}}dite{{/eq}} "{{employee_pseudo}}"{{/ne}}</li>
<li>
{{#eq employee_civ "Monsieur"}}né{{else}}née{{/eq}}
le {{ employee_dob }}
{{#if (contains employee_cob "Le ")}}
au {{removeFirst employee_cob "Le "}}
{{else}}
à {{ employee_cob }}
{{/if}}
</li>
<li>demeurant {{employee_address}}</li>
{{#if (or (eq employee_ss 0) (isEmpty employee_ss))}}
<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>
{{/if}}
<li>N° Congés Spectacles : {{employee_cs}}</li>
{{#eq mineur1618 "Oui"}}<li>dont {{#eq representant_civ "Monsieur"}}le représentant légal{{else}}la représentante légale{{/eq}} est {{representant_civ}} {{representant_nom}}, {{#eq representant_civ "Monsieur"}}né{{else}}née{{/eq}} le {{representant_dob}} à {{representant_cob}}, demeurant {{representant_adresse}}.</li>{{/eq}}
</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 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 {{join CCN ', '}}{{#if (contains (join CCN ', ') "Convention Collective Nationale de l'Édition")}} et de ses annexes afférentes à l'Édition Phonographique{{/if}}.
</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 {{#eq employee_civ "Monsieur"}}engagé{{else}}engagée{{/eq}} selon l'objet suivant :
<ul>
<li><b>Profession</b> : {{employee_profession}}</li>
<li><b>Code emploi</b> : {{employee_codeprofession}}</li>
<li>
{{#if (and (eq structure_spectacle "Oui") (ne type_numobjet "Administratif"))}}
<b>Spectacle</b> : {{ spectacle }}
{{else if (includesAny (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle" "Convention Collective Nationale de l'Édition")}}
<b>Production</b> : {{ spectacle }}
{{/if}}
</li>
<li>{{#isNotEmpty numobjet}}<b>Numéro d'objet</b> : {{numobjet}}{{else}}Le <b>numéro d'objet</b> de cette production est en cours d'attribution.{{/isNotEmpty}}</li>
</ul>
</div>
<div class="section-objet">
<h2 class="section-title">DURÉE DE L'ENGAGEMENT</h2>
<p>
{{#eq date_debut date_fin}}
Le présent engagement couvre la journée du {{ date_debut }}, pour
{{else}}
{{#isNotEmpty dates_travaillees}}
Le présent engagement couvre la période du {{ date_debut }} au {{ date_fin }} pour les dates travaillées suivantes :
<ul>
{{#each (split dates_travaillees ";")}}
<li>- {{strip this}}{{#unless @last}};{{/unless}}</li>
{{/each}}
</ul>
{{else}}
Le présent engagement couvre la période du {{ date_debut }} au {{ date_fin }}.
{{/isNotEmpty}}
Pour
{{/eq}}
{{#eq employee_catpro "Artiste"}}
un total de
{{#if (and (gte cachets.representations 1) (gte cachets.repetitions 1))}}
{{cachets.representations}} {{#eq cachets.representations 1}}cachet{{else}}cachets{{/eq}} de représentation et {{cachets.repetitions}} {{#eq cachets.repetitions 1}}service{{else}}services{{/eq}} de répétition.
{{/if}}
{{#if (and (gte cachets.representations 1) (eq cachets.repetitions 0))}}
{{cachets.representations}} {{#eq cachets.representations 1}}cachet{{else}}cachets{{/eq}}{{#if (includesAny (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle" "Convention Collective Nationale de l'Édition")}} d'enregistrement.{{else}} de représentation.{{/if}}
{{/if}}
{{#if (and (eq cachets.representations 0) (gte cachets.repetitions 1))}}
{{cachets.repetitions}} {{#eq cachets.repetitions 1}}service{{else}}services{{/eq}} de répétition.
{{/if}}
{{/eq}}
{{#eq employee_catpro "Technicien"}}
un total de {{cachets.heures}} heures de travail{{#eq cachets.heuresparjour 0}}.{{/eq}}{{#gte cachets.heuresparjour 1}}, à raison de {{cachets.heuresparjour}} heures par jour de travail.{{/gte}}
{{/eq}}
{{#eq employee_catpro "Metteur en scène"}}
{{#if (and (gte cachets.representations 1) (gt cachets.heures 0))}}
un total de {{cachets.representations}} {{#eq cachets.representations 1}}cachet{{else}}cachets{{/eq}} de représentation et {{cachets.heures}} heures de travail.
{{/if}}
{{#if (eq cachets.representations 0)}}
un total de {{cachets.heures}} heures de travail.
{{/if}}
{{#if (and (gte cachets.representations 1) (eq cachets.heures 0))}}
un total de {{cachets.representations}} {{#eq cachets.representations 1}}cachet{{else}}cachets{{/eq}} de représentation.
{{/if}}
{{/eq}}
</p>
<p>
{{#if (gte cachets.repetitions 1)}}La durée totale des répétitions sera de {{cachets.heures}} heures{{#eq cachets.heuresparjour 0}}.{{/eq}}{{#gte cachets.heuresparjour 1}}, à raison de {{cachets.heuresparjour}} heures par journée de répétition.{{/gte}}{{/if}}
</p>
{{#isNotEmpty autreprecision_duree}}<p>{{autreprecision_duree}}</p>{{/isNotEmpty}}
</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 (includesAny (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle" "Convention Collective Nationale de l'Édition")}}
de travail
{{else if (and (gte cachets.representations 1) (eq cachets.repetitions 0))}}
des représentations
{{else if (and (eq cachets.representations 0) (gte cachets.repetitions 1))}}
des répétitions
{{else if (eq employee_catpro "Technicien")}}
d'engagement
{{else if (and (gte cachets.representations 1) (gte cachets.repetitions 1))}}
des répétitions et des représentations
{{/if}}
{{#if (and (eq employee_catpro "Metteur en scène") (eq cachets.representations 0))}}d'exercice de sa fonction{{/if}}, 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>
{{#isNotEmpty precisions_salaire}}
<p>
À titre informatif, la répartition de ce salaire brut est la suivante : {{precisions_salaire}}.
</p>
{{/isNotEmpty}}
{{#if (and (isNotEmpty panierrepas) (isNotEmpty hebergement))}}
<p>{{employee_firstname}} {{employee_lastname}} percevra {{panierrepas}}
{{#eq panierrepas "1"}}panier repas principal{{else}}paniers repas principaux,{{/eq}} et {{hebergement}} {{#eq hebergement "1"}}indemnité{{else}}indemnités{{/eq}} d'hébergement et petit-déjeuner,
{{#if (and (eq panierrepasccn "Oui") (eq hebergementccn "Oui"))}}
selon les conditions prévues par la Convention Collective.
{{else if (and (eq panierrepasccn "Non") (eq 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.
{{else if (and (eq panierrepasccn "Oui") (eq 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.
{{else if (and (eq panierrepasccn "Non") (eq hebergementccn "Non"))}}
à hauteur de {{montantpanierrepas}} euros par panier repas principal et à hauteur de {{montanthebergement}} euros par indemnité hébergement et petit-déjeuner.
{{/if}}
</p>
{{/if}}
{{#isNotEmpty autreprecision_salaire}}<p>{{autreprecision_salaire}}</p>{{/isNotEmpty}}
</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 (eq employee_catpro "Metteur en scène")}}
ses missions de mise en scène,
{{else if (contains (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle")}}
ses missions de {{employee_profession}},
{{else if (contains (join CCN ', ') "Convention Collective Nationale de l'Édition")}}
un enregistrement,
{{else}}
une répétition ou une représentation,
{{/if}}
{{employee_firstname}} {{employee_lastname}} sera {{#eq employee_civ "Monsieur"}}tenu{{else}}tenue{{/eq}}
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 (eq employee_catpro "Metteur en scène")}}
sur ses lieux de travail et aux répétitions
{{else if (contains (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle")}}
sur les lieux de production
{{else if (contains (join CCN ', ') "Convention Collective Nationale de l'Édition")}}
sur les lieux d'enregistrement
{{else}}
à une répétition ou à une représentation
{{/if}}
pour cause d'engagement extérieur, à quelque moment qu'il·elle ait été prévenu
{{#if (eq employee_catpro "Metteur en scène")}}
de ses horaires et jours de travail et de l'existence de répétitons.
{{else if (contains (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle")}}
de ses horaires, jours et lieux de travail.
{{else if (contains (join CCN ', ') "Convention Collective Nationale de l'Édition")}}
de cet session d'enregistrement.
{{else}}
de l'existence de cette répétition ou représentation.
{{/if}}
</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 {{#eq employee_civ "Monsieur"}}tenu{{else}}tenue{{/eq}} 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 (contains (join CCN ', ') "Convention Collective Nationale de la Production Audiovisuelle")}} à la production audiovisuelle.{{else if (contains (join CCN ', ') "Convention Collective Nationale de l'Édition")}} à l'édition phonographique.{{else}} aux représentations du spectacle.{{/if}}
</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>
<p>
{{#if (contains structure_ville "Le ")}}
Au {{removeFirst structure_ville "Le "}}, le {{ date_signature }}.
{{else}}
À {{ structure_ville }}, le {{ date_signature }}.
{{/if}}
</p>
</div>
<div class="info-paragraph">
<div class="info-row">
<div class="info-label">{{#eq employee_civ "Monsieur"}}Le salarié :{{else}}La salariée :{{/eq}}</div>
<div class="info-value">{{employee_civ}} {{employee_firstname}} {{employee_lastname}}</div>
<br>
<div>[Signature Employé - À intégrer via Docuseal]</div>
<br><br><br><br>
</div>
{{#eq mineur1618 "Oui"}}
<div class="info-row">
<div class="info-label">{{#eq representant_civ "Monsieur"}}Le représentant légal{{else}}La représentante légale{{/eq}}{{#eq employee_civ "Madame"}} de la salariée :{{else}} du salarié :{{/eq}}</div>
<div class="info-value">{{representant_civ}} {{representant_nom}}</div>
<div class="info-signature">[Signature Représentant - À intégrer via Docuseal]</div>
<br><br>
</div>
{{/eq}}
<div class="info-row">
<div class="info-label">L'employeur:</div>
<div class="info-value">Pour {{structure_name}},</div>
{{#eq delegation "Oui"}}<div class="info-delegation">Pour le représentant légal et par délégation,</div>{{/eq}}
<div class="info-value">{{structure_signataire}},</div>
<div class="info-value">{{structure_signatairequalite}}.</div>
<br>
<div>[Signature Employeur - À intégrer via Docuseal]</div>
</div>
</div>
</body>
</html>