diff --git a/POSTHOG_ANALYTICS_GUIDE.md b/POSTHOG_ANALYTICS_GUIDE.md index fa1809f..d7a7d9d 100644 --- a/POSTHOG_ANALYTICS_GUIDE.md +++ b/POSTHOG_ANALYTICS_GUIDE.md @@ -84,6 +84,21 @@ function MonComposant() { } ``` +**Exemple concret : Tracking de création de contrat** ✨ + +```tsx +// Après création réussie d'un contrat +posthog?.capture('contract_created', { + contract_type: 'CDDU', + regime: 'CDDU_MONO', + categorie_professionnelle: 'Artiste', + contract_id: contractId, + validation_immediate: true, +}); +``` + +> 💡 **Voir le guide complet** : `POSTHOG_SURVEY_CONTRATS.md` pour la mise en place d'un survey de satisfaction après création de contrats. + ### Identifier un utilisateur manuellement ```typescript diff --git a/POSTHOG_SURVEY_CHECKLIST.md b/POSTHOG_SURVEY_CHECKLIST.md new file mode 100644 index 0000000..b87324f --- /dev/null +++ b/POSTHOG_SURVEY_CHECKLIST.md @@ -0,0 +1,300 @@ +# ✅ Survey PostHog - Checklist complète + +## 🎯 Objectif +Mesurer la satisfaction des utilisateurs sur le processus de création de contrats avec une note de 1 à 5. + +--- + +## ✅ Ce qui a été fait + +### 1. ✅ Code frontend modifié +- **Fichier** : `components/contrats/NouveauCDDUForm.tsx` +- **Modification** : Import de `usePostHog` et tracking de l'événement `contract_created` +- **Événement envoyé** : Contient `contract_type`, `regime`, `categorie_professionnelle`, etc. + +### 2. ✅ Composant survey créé (optionnel) +- **Fichier** : `components/surveys/ContractCreationSurvey.tsx` +- **Usage** : Si vous voulez un contrôle total sur le design +- **Avantage** : Customisable à 100% + +### 3. ✅ Documentation créée +- `POSTHOG_SURVEY_CONTRATS.md` - Guide complet du survey +- `POSTHOG_SURVEY_INTEGRATION_GUIDE.md` - Guide d'intégration rapide +- `POSTHOG_ANALYTICS_GUIDE.md` - Mis à jour avec référence au survey + +--- + +## 🚀 Prochaines étapes (à faire par vous) + +### Étape 1 : Vérifier l'environnement (5 min) + +Vérifiez que ces variables sont dans votre `.env.local` : + +```bash +NEXT_PUBLIC_POSTHOG_KEY=phc_... +NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com +``` + +Si elles manquent, ajoutez-les et **redémarrez le serveur** : + +```bash +npm run dev +``` + +### Étape 2 : Tester l'événement (10 min) + +1. Créez un contrat de test +2. Ouvrez la **console du navigateur** (F12) +3. Vous devriez voir : `📊 PostHog: Événement contract_created envoyé` +4. Vérifiez dans PostHog → **Activity** → **Live Events** +5. Cherchez l'événement `contract_created` - il devrait apparaître dans les 30 secondes + +### Étape 3 : Créer le survey dans PostHog (15 min) + +#### 🔧 Configuration du survey + +1. **Aller sur PostHog** : https://eu.posthog.com/ +2. **Menu** → **Surveys** → **New survey** +3. **Type** : Choisir **"Rating"** + +#### 📝 Paramètres du survey + +**Question** : +``` +Comment évaluez-vous la facilité du processus de création de contrat ? +``` + +**Type de réponse** : +- **Scale** : 1 to 5 +- **Display** : Stars ⭐ (ou Numbers 1-5) +- **Labels** : + - Low (1) : Très difficile + - High (5) : Très facile + +**Nom du survey** : +``` +Satisfaction création de contrats +``` + +#### 🎯 Ciblage (Targeting) + +**Option simple (recommandée pour commencer)** : + +``` +Event name: contract_created +Show when: Event is captured +Wait: 2 seconds +Position: Bottom right +Frequency: Show every time (pour avoir plusieurs réponses) +Days between appearances: 7 (ne pas harceler) +``` + +**Option avancée (après X créations)** : + +``` +Event name: contract_created +Condition: User has completed 3+ contract_created events +Show when: Event is captured +``` + +#### 🎨 Apparence + +``` +Title: 📊 Votre avis compte ! +Question: Comment évaluez-vous la facilité du processus de création de contrat ? +Button text: Envoyer +Dismiss text: Plus tard +Primary color: #10b981 (vert de votre app) +``` + +#### ➕ Question de suivi (optionnelle) + +Si note ≤ 3 : +``` +Question: Que pourrions-nous améliorer ? +Type: Open text +Placeholder: Partagez vos suggestions... +``` + +4. **Sauvegarder** et **Activer** le survey + +### Étape 4 : Tester le survey (5 min) + +1. Créez un nouveau contrat +2. Le survey devrait apparaître 2 secondes après la création +3. Donnez une note +4. Vérifiez dans PostHog → **Surveys** → **Votre survey** que la réponse apparaît + +--- + +## 📊 Analyser les résultats + +### Dashboard automatique + +PostHog crée automatiquement un dashboard pour votre survey avec : +- **Response rate** : Taux de réponse +- **Average rating** : Note moyenne +- **Distribution** : Répartition 1-5 +- **Responses over time** : Évolution + +### Insights personnalisés + +Créez des insights pour analyser plus en détail : + +**1. Note moyenne par type de contrat** +``` +Event: survey_responded +Survey: Satisfaction création de contrats +Breakdown: contract_type +Aggregation: Average rating +``` + +**2. Taux de réponse par catégorie professionnelle** +``` +Event: survey_responded +Survey: Satisfaction création de contrats +Breakdown: categorie_professionnelle +Aggregation: Count +``` + +**3. Évolution de la satisfaction dans le temps** +``` +Event: survey_responded +Survey: Satisfaction création de contrats +Aggregation: Average rating +Graph: Line chart (par semaine) +``` + +### Alertes + +Configurez des alertes pour être notifié : + +``` +Condition: Average rating < 3.0 (sur les 7 derniers jours) +Action: Envoyer email à l'équipe produit +``` + +--- + +## 🎨 Option alternative : Survey personnalisé + +Si vous préférez un contrôle total sur le design, utilisez le composant React créé. + +### Intégration rapide + +Dans `NouveauCDDUForm.tsx`, ajoutez : + +```tsx +import { ContractCreationSurvey } from '@/components/surveys/ContractCreationSurvey'; + +// States +const [showSurvey, setShowSurvey] = useState(false); +const [surveyContractId, setSurveyContractId] = useState(); + +// Après création réussie +setSurveyContractId(result?.contract?.id); +setTimeout(() => setShowSurvey(true), 1500); + +// Dans le JSX + setShowSurvey(false)} + contractId={surveyContractId} + contractType={isRegimeRG ? 'RG' : 'CDDU'} +/> +``` + +Voir `POSTHOG_SURVEY_INTEGRATION_GUIDE.md` pour les détails. + +--- + +## 🔍 Troubleshooting + +### Le survey ne s'affiche pas + +**1. Vérifier que l'événement arrive bien** +- PostHog → Activity → Live Events +- Chercher `contract_created` +- Si absent → vérifier les variables d'environnement + +**2. Vérifier le ciblage du survey** +- PostHog → Surveys → Votre survey → Targeting +- S'assurer que l'événement `contract_created` est bien configuré + +**3. Vérifier les conditions** +- Si vous avez mis une condition (ex: "after 3 contracts"), créez suffisamment de contrats de test + +### L'événement n'arrive pas dans PostHog + +**1. Console du navigateur** +```javascript +window.posthog.debug(true) +// Créez un contrat +// Vous devriez voir des logs PostHog +``` + +**2. Network tab** +- DevTools → Network +- Filtrer par "batch" +- Vérifier statut 200 + +**3. Variables d'environnement** +```bash +# Vérifier qu'elles sont bien définies +echo $NEXT_PUBLIC_POSTHOG_KEY +echo $NEXT_PUBLIC_POSTHOG_HOST + +# Si vides, les ajouter à .env.local et redémarrer +``` + +--- + +## 📈 Métriques à surveiller + +### Court terme (premières semaines) +- **Taux de réponse** : Objectif > 30% +- **Note moyenne** : Objectif > 4.0/5 +- **% de notes ≤ 3** : À minimiser + +### Moyen terme (après 1 mois) +- **Évolution de la note** : Doit augmenter +- **Feedback qualitatif** : Identifier les points de friction récurrents +- **Corrélations** : Note vs type de contrat, catégorie pro, etc. + +### Actions selon les résultats + +| Note moyenne | Action | +|--------------|--------| +| ≥ 4.5 | 🎉 Excellent ! Continuez comme ça | +| 4.0 - 4.4 | ✅ Bien. Identifier les petites améliorations | +| 3.0 - 3.9 | ⚠️ Moyen. Analyser les feedbacks et prioriser des améliorations | +| < 3.0 | 🚨 Urgent. Revoir le processus de création | + +--- + +## 📚 Ressources + +- **PostHog Surveys** : https://posthog.com/docs/surveys +- **Best practices** : https://posthog.com/docs/surveys/best-practices +- **Survey templates** : https://posthog.com/templates/surveys +- **Guide interne** : `POSTHOG_SURVEY_CONTRATS.md` + +--- + +## ✅ Checklist finale + +- [ ] Variables PostHog dans `.env.local` +- [ ] Serveur redémarré après ajout des variables +- [ ] Test : création d'un contrat → événement `contract_created` dans PostHog +- [ ] Survey créé dans PostHog dashboard +- [ ] Ciblage configuré sur `contract_created` +- [ ] Test : création d'un contrat → survey s'affiche +- [ ] Test : répondre au survey → réponse enregistrée +- [ ] Dashboard de résultats vérifié +- [ ] (Optionnel) Alertes configurées + +--- + +**Temps estimé total** : 30-45 minutes + +**Dernière mise à jour** : 15 octobre 2025 diff --git a/POSTHOG_SURVEY_CONTRATS.md b/POSTHOG_SURVEY_CONTRATS.md new file mode 100644 index 0000000..c05e018 --- /dev/null +++ b/POSTHOG_SURVEY_CONTRATS.md @@ -0,0 +1,319 @@ +# 📊 Survey PostHog : Satisfaction création de contrats + +## 🎯 Objectif + +Mesurer la satisfaction des utilisateurs concernant le processus de création de contrats avec une note de 1 à 5. + +## ✅ Étape 1 : Événement tracké + +L'événement `contract_created` est maintenant envoyé à PostHog après chaque création de contrat réussie, avec les propriétés suivantes : + +```typescript +posthog.capture('contract_created', { + contract_type: 'CDDU' | 'RG', + regime: 'CDDU_MONO' | 'CDDU_MULTI' | 'Régime Général', + multi_mois: boolean, + categorie_professionnelle: 'Artiste' | 'Technicien' | 'Autre', + contract_id: string, + has_notes: boolean, + validation_immediate: boolean, +}); +``` + +## 🔧 Étape 2 : Créer le survey dans PostHog + +### 1. Accéder à PostHog +- Rendez-vous sur https://eu.posthog.com/ +- Connectez-vous avec vos identifiants + +### 2. Créer un nouveau survey +1. Dans le menu de gauche, cliquez sur **"Surveys"** +2. Cliquez sur **"New survey"** +3. Choisissez **"Rating"** comme type de question + +### 3. Configuration du survey + +#### **Question** +``` +Comment évaluez-vous la facilité du processus de création de contrat ? +``` + +#### **Type de réponse** +- **Type** : Rating (échelle) +- **Scale** : 1 à 5 +- **Display** : Stars (⭐) ou Numbers (1 2 3 4 5) + +#### **Labels optionnels** +- **1 étoile** : Très difficile +- **5 étoiles** : Très facile + +ou simplement : +- **Low** : 1 - Difficile +- **High** : 5 - Facile + +#### **Nom du survey** +``` +Satisfaction création de contrats +``` + +#### **Description (optionnelle)** +``` +Aidez-nous à améliorer votre expérience +``` + +### 4. Ciblage du survey + +#### **Quand afficher le survey ?** + +**Option A : Après chaque création (recommandé pour commencer)** +``` +Événement : contract_created +Affichage : Immédiatement après l'événement +``` + +**Option B : Après X créations (pour ne pas être trop intrusif)** +``` +Événement : contract_created +Condition : event_count >= 3 +(Afficher seulement après 3 créations de contrats) +``` + +**Option C : Une seule fois par utilisateur** +``` +Événement : contract_created +Condition : survey_not_completed +Fréquence : Une fois par utilisateur +``` + +#### **Configuration recommandée pour démarrer :** + +Dans la section **"Targeting"** du survey PostHog : + +1. **Linked flag or targeting** : + - Event name: `contract_created` + +2. **Conditions** : + - Show survey when: `contract_created` event is captured + - Wait: `1 second` (laisser le temps à la redirection de se faire) + +3. **Appearance** : + - Position: Bottom right (en bas à droite) + - Type: Popover (fenêtre flottante) + +4. **Frequency** : + - Show once per user: **No** (pour avoir plusieurs réponses) + - Days between survey appearances: **7** (ne pas harceler l'utilisateur) + +### 5. Design et style + +``` +Title: 📊 Votre avis compte ! +Question: Comment évaluez-vous la facilité du processus de création de contrat ? +Button text: Envoyer +Dismiss button: Peut-être plus tard +``` + +**Couleurs** : +- Primary color: `#10b981` (vert Emerald de votre app) +- Background: `#ffffff` + +### 6. Question de suivi (optionnelle) + +Vous pouvez ajouter une **question de suivi** si la note est basse (≤ 3) : + +``` +Si note ≤ 3 : "Que pourrions-nous améliorer ?" +Type : Open text (texte libre) +Placeholder : "Partagez vos suggestions..." +``` + +Configuration dans PostHog : +- Cliquez sur **"Add follow-up question"** +- Condition: `rating <= 3` +- Type: Open text + +## 📈 Étape 3 : Analyser les résultats + +### Dashboard dans PostHog + +1. Aller dans **Surveys** → **Votre survey** +2. Consulter : + - **Response rate** : Taux de réponse + - **Average rating** : Note moyenne + - **Distribution** : Répartition des notes (1 à 5) + - **Responses over time** : Évolution dans le temps + +### Créer des insights personnalisés + +1. Aller dans **Insights** → **New insight** +2. Créer un graphique : + ``` + Event: survey_responded + Filter: survey_name = "Satisfaction création de contrats" + Breakdown: rating + ``` + +3. Analyser par type de contrat : + ``` + Event: survey_responded + Filter: survey_name = "Satisfaction création de contrats" + Breakdown: contract_type (CDDU vs RG) + ``` + +### Alertes automatiques + +Configurez une alerte si la note moyenne descend sous un seuil : + +1. Aller dans **Alerts** → **New alert** +2. Condition : `average_rating < 3` +3. Notification : Email ou Slack + +## 🎨 Option avancée : Survey personnalisé dans le code + +Si vous préférez un contrôle total sur le design et le timing, vous pouvez créer un survey personnalisé : + +```tsx +// components/ContractCreationSurvey.tsx +'use client'; + +import { useState } from 'react'; +import { usePostHog } from 'posthog-js/react'; +import { Star, X } from 'lucide-react'; + +export function ContractCreationSurvey({ + isOpen, + onClose, + contractId +}: { + isOpen: boolean; + onClose: () => void; + contractId?: string; +}) { + const posthog = usePostHog(); + const [rating, setRating] = useState(null); + const [hoverRating, setHoverRating] = useState(null); + const [submitted, setSubmitted] = useState(false); + + if (!isOpen || submitted) return null; + + const handleSubmit = () => { + if (rating) { + posthog?.capture('contract_creation_survey_submitted', { + rating, + contract_id: contractId, + }); + setSubmitted(true); + setTimeout(onClose, 2000); + } + }; + + return ( +
+ + +

📊 Votre avis compte !

+

+ Comment évaluez-vous la facilité du processus de création de contrat ? +

+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ +
+ Très difficile + Très facile +
+ + +
+ ); +} +``` + +Puis l'intégrer dans `NouveauCDDUForm.tsx` : + +```tsx +const [showSurvey, setShowSurvey] = useState(false); +const [createdContractId, setCreatedContractId] = useState(); + +// Après création réussie : +setCreatedContractId(result?.contract?.id); +setShowSurvey(true); + +// Dans le JSX : + setShowSurvey(false)} + contractId={createdContractId} +/> +``` + +## 📊 Comparaison des deux approches + +| Critère | Survey PostHog natif | Survey personnalisé | +|---------|---------------------|---------------------| +| **Facilité de mise en place** | ⭐⭐⭐⭐⭐ Très facile | ⭐⭐⭐ Moyen | +| **Contrôle du design** | ⭐⭐⭐ Limité | ⭐⭐⭐⭐⭐ Total | +| **Analytics automatiques** | ⭐⭐⭐⭐⭐ Intégré | ⭐⭐⭐ À configurer | +| **A/B testing** | ⭐⭐⭐⭐⭐ Natif | ⭐⭐ Manuel | +| **Multilingue** | ⭐⭐⭐⭐ Facile | ⭐⭐⭐⭐⭐ Total | +| **Maintenance** | ⭐⭐⭐⭐⭐ Aucune | ⭐⭐⭐ Régulière | + +**Recommandation** : Commencez avec le **Survey PostHog natif**, c'est plus rapide et vous aurez les analytics automatiquement. Si vous avez besoin de plus de contrôle, passez au survey personnalisé plus tard. + +## ✅ Checklist de déploiement + +- [ ] Code modifié dans `NouveauCDDUForm.tsx` (déjà fait ✅) +- [ ] Variables PostHog configurées dans `.env.local` +- [ ] Créer le survey dans le dashboard PostHog +- [ ] Configurer le ciblage sur l'événement `contract_created` +- [ ] Tester en créant un contrat +- [ ] Vérifier dans PostHog que l'événement arrive +- [ ] Vérifier que le survey s'affiche +- [ ] Configurer les alertes si nécessaire + +## 🧪 Test + +1. Créez un contrat de test +2. Vérifiez dans la console : `📊 PostHog: Événement contract_created envoyé` +3. Le survey devrait apparaître quelques secondes après la création +4. Donnez une note +5. Vérifiez dans PostHog → Surveys que la réponse est enregistrée + +## 📚 Ressources + +- [Documentation PostHog Surveys](https://posthog.com/docs/surveys) +- [Survey templates](https://posthog.com/templates/surveys) +- [Best practices](https://posthog.com/docs/surveys/best-practices) + +--- + +**Dernière mise à jour** : 15 octobre 2025 diff --git a/POSTHOG_SURVEY_INTEGRATION_GUIDE.md b/POSTHOG_SURVEY_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..60e97a1 --- /dev/null +++ b/POSTHOG_SURVEY_INTEGRATION_GUIDE.md @@ -0,0 +1,189 @@ +# 🎯 Guide rapide : Intégration du survey personnalisé + +## Option 1 : Utiliser le survey PostHog natif (Recommandé) + +✅ **Déjà configuré !** L'événement `contract_created` est envoyé automatiquement. + +Il vous suffit de : +1. Aller sur https://eu.posthog.com/ +2. Créer un survey (voir `POSTHOG_SURVEY_CONTRATS.md`) +3. Le cibler sur l'événement `contract_created` + +**Avantages** : Analytics automatiques, A/B testing, maintenance zéro. + +--- + +## Option 2 : Utiliser le composant personnalisé + +Si vous voulez un contrôle total sur le design et le timing, utilisez le composant `ContractCreationSurvey`. + +### Intégration dans NouveauCDDUForm + +Ajoutez ces imports en haut du fichier : + +```tsx +import { ContractCreationSurvey } from '@/components/surveys/ContractCreationSurvey'; +``` + +Ajoutez ces states avec les autres states du formulaire : + +```tsx +const [showSurvey, setShowSurvey] = useState(false); +const [surveyContractId, setSurveyContractId] = useState(); +const [surveyContractType, setSurveyContractType] = useState<'CDDU' | 'RG'>(); +``` + +Dans la fonction `onSubmit`, après la création réussie, ajoutez : + +```tsx +// Après: const result = await res.json().catch(() => ({})); +setSurveyContractId(result?.contract?.id || result?.contract?.contract_number); +setSurveyContractType(isRegimeRG ? 'RG' : 'CDDU'); + +// Afficher le survey après un court délai (laisser le temps à la redirection de commencer) +setTimeout(() => { + setShowSurvey(true); +}, 1500); +``` + +Ajoutez le composant dans le JSX, après les overlays existants : + +```tsx +{/* Survey de satisfaction - après la fin des overlays existants */} + setShowSurvey(false)} + contractId={surveyContractId} + contractType={surveyContractType} +/> +``` + +### Exemple complet de modification + +```tsx +// Dans onSubmit(), juste après la création réussie : + +const result = await res.json().catch(() => ({})); + +// 🎯 PostHog event (déjà en place) +posthog?.capture('contract_created', { ... }); + +// 🎯 Préparer le survey personnalisé +setSurveyContractId(result?.contract?.id); +setSurveyContractType(isRegimeRG ? 'RG' : 'CDDU'); + +// Autoriser la navigation +allowNavRef.current = true; +setRedirecting(true); + +// Afficher le survey après 1.5s (pendant la redirection) +setTimeout(() => { + setShowSurvey(true); +}, 1500); + +// Redirection après 3s +setTimeout(() => { + router.push("/contrats"); +}, 3000); +``` + +### Résultat + +Le survey apparaîtra en **bas à droite** de l'écran pendant la redirection, permettant à l'utilisateur de donner son avis avant d'être redirigé vers la liste des contrats. + +--- + +## 📊 Analyser les résultats + +### Dans PostHog + +1. Allez dans **Activity** → **Live events** +2. Filtrez par événement : `contract_creation_survey_submitted` +3. Vous verrez les propriétés : + - `rating` : Note de 1 à 5 + - `feedback` : Commentaire (si donné) + - `contract_id` : ID du contrat + - `contract_type` : CDDU ou RG + +### Créer un insight + +``` +Event: contract_creation_survey_submitted +Aggregation: Average of rating +Breakdown: contract_type +``` + +Cela vous donnera la note moyenne par type de contrat. + +--- + +## 🎨 Personnalisation + +### Changer les couleurs + +Dans `ContractCreationSurvey.tsx`, modifiez les classes Tailwind : + +```tsx +// Bouton principal +className="... bg-emerald-600 hover:bg-emerald-700" + +// Étoiles +className="... fill-yellow-400 text-yellow-400" + +// Border du composant +className="... border-slate-200" +``` + +### Changer la position + +```tsx +// En bas à gauche +className="fixed bottom-4 left-4 ..." + +// En haut à droite +className="fixed top-4 right-4 ..." + +// Centre de l'écran +className="fixed inset-0 flex items-center justify-center ..." +``` + +### Changer le timing + +```tsx +// Afficher immédiatement après création +setTimeout(() => setShowSurvey(true), 0); + +// Afficher après 5 secondes +setTimeout(() => setShowSurvey(true), 5000); +``` + +--- + +## 🚨 Troubleshooting + +### Le survey ne s'affiche pas + +1. Vérifiez que `isOpen={true}` dans les props +2. Vérifiez que PostHog est bien initialisé (`window.posthog` dans la console) +3. Vérifiez les z-index (le composant a `z-50`) + +### Le survey se ferme trop vite + +Augmentez le délai dans `handleSubmit` : + +```tsx +setTimeout(() => { + onClose(); +}, 3000); // 3 secondes au lieu de 2 +``` + +### Les événements n'arrivent pas dans PostHog + +1. Ouvrez DevTools → Network +2. Filtrez par "batch" +3. Vérifiez que les requêtes vers PostHog ont un statut 200 +4. Vérifiez dans la console : `📊 Survey soumis: ...` + +--- + +**Recommandation finale** : Commencez avec le **survey PostHog natif** pour sa simplicité et ses analytics automatiques. Passez au composant personnalisé seulement si vous avez des besoins spécifiques de design ou de UX. diff --git a/app/(app)/contrats-multi/[id]/page.tsx b/app/(app)/contrats-multi/[id]/page.tsx index 738a313..1ea34c4 100644 --- a/app/(app)/contrats-multi/[id]/page.tsx +++ b/app/(app)/contrats-multi/[id]/page.tsx @@ -1,15 +1,19 @@ "use client"; import Link from "next/link"; +import Script from "next/script"; import { useParams } from "next/navigation"; -import { useMemo, useState } from "react"; +import { useMemo, useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Download, Info, Loader2, Clock, CheckCircle, Euro } from "lucide-react"; +import { ArrowLeft, Download, Info, Loader2, Clock, CheckCircle, Euro, PenTool, XCircle, Users, Send, AlertTriangle, X, ExternalLink, AlertCircle } from "lucide-react"; import { NotesSection } from "@/components/NotesSection"; import { Button } from "@/components/ui/button"; import { ConfirmationModal } from "@/components/ui/confirmation-modal"; +import { LoadingModal } from "@/components/ui/loading-modal"; +import DocumentsCard from "@/components/contrats/DocumentsCard"; import { toast } from "sonner"; import { usePageTitle } from "@/hooks/usePageTitle"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; /* ========================= Types attendus du backend @@ -458,9 +462,84 @@ export default function ContratMultiPage() { const [selectedPaieId, setSelectedPaieId] = useState(""); const [selectedPaieStatus, setSelectedPaieStatus] = useState(false); + // State pour la modale de signature DocuSeal + const [embedSrc, setEmbedSrc] = useState(""); + const [modalTitle, setModalTitle] = useState(""); + const [signatureB64ForDocuSeal, setSignatureB64ForDocuSeal] = useState(null); + + // State pour la modale de chargement + const [isLoadingSignature, setIsLoadingSignature] = useState(false); + + // State pour la modale d'erreur DocuSeal + const [showErrorModal, setShowErrorModal] = useState(false); + + // State pour la modale de visualisation des fiches de paie + const [isPayslipModalOpen, setIsPayslipModalOpen] = useState(false); + const [currentPayslipUrl, setCurrentPayslipUrl] = useState(""); + const [currentPayslipTitle, setCurrentPayslipTitle] = useState(""); + const [payslipPdfError, setPayslipPdfError] = useState(false); + // Query client pour la mise à jour du cache const queryClient = useQueryClient(); + // Effet pour bloquer le défilement quand le modal DocuSeal est ouvert + useEffect(() => { + // Vérifier si le dialog est ouvert en surveillant embedSrc + const dlg = document.getElementById('dlg-signature') as HTMLDialogElement | null; + if (dlg && embedSrc) { + // Bloquer le défilement du body + document.body.style.overflow = 'hidden'; + + // Observer pour détecter la fermeture du dialog + const observer = new MutationObserver(() => { + if (!dlg.open) { + document.body.style.overflow = ''; + } + }); + + observer.observe(dlg, { attributes: true, attributeFilter: ['open'] }); + + return () => { + observer.disconnect(); + document.body.style.overflow = ''; + }; + } else { + // S'assurer que le défilement est rétabli si embedSrc est vide + document.body.style.overflow = ''; + } + }, [embedSrc]); + + // Effet pour bloquer le défilement quand le modal de fiche de paie est ouvert + useEffect(() => { + if (isPayslipModalOpen) { + document.body.style.overflow = 'hidden'; + + // Handler pour fermer avec Escape + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setIsPayslipModalOpen(false); + } + }; + + document.addEventListener('keydown', handleEscape); + + return () => { + document.body.style.overflow = ''; + document.removeEventListener('keydown', handleEscape); + }; + } else { + document.body.style.overflow = ''; + } + }, [isPayslipModalOpen]); + + // Fonction pour ouvrir une fiche de paie dans le modal + const openPayslipInModal = (url: string, title: string) => { + setCurrentPayslipUrl(url); + setCurrentPayslipTitle(title); + setPayslipPdfError(false); + setIsPayslipModalOpen(true); + }; + // Mutation pour marquer une paie multi comme payée/non payée const markAsPaidMutation = useMutation({ mutationFn: async ({ payslipId, transferDone }: { payslipId: string; transferDone: boolean }) => { @@ -579,6 +658,213 @@ export default function ContratMultiPage() { ); } + // Fonction pour déterminer l'état de la signature électronique + const getSignatureStatus = () => { + const etatDemande = data.etat_demande; + const contratSigneEmployeur = data.contrat_signe_employeur; + const contratSigne = data.contrat_signe_salarie; + + // Détermine le statut + if (contratSigneEmployeur === "oui" && contratSigne === "oui") { + return { + status: "completed" as const, + label: "Complété", + icon: CheckCircle, + color: "text-green-600", + bgColor: "bg-green-50", + borderColor: "border-green-200" + }; + } else if (contratSigneEmployeur === "oui" && contratSigne !== "oui") { + return { + status: "waiting_employee" as const, + label: "En attente salarié", + icon: Users, + color: "text-blue-600", + bgColor: "bg-blue-50", + borderColor: "border-blue-200" + }; + } else if (String(etatDemande || "").toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').includes("traitee") || String(etatDemande || "").toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').includes("traitée")) { + return { + status: "waiting_employer" as const, + label: "En attente employeur", + icon: Send, + color: "text-orange-600", + bgColor: "bg-orange-50", + borderColor: "border-orange-200" + }; + } else { + return { + status: "not_sent" as const, + label: "Non envoyé", + icon: Clock, + color: "text-gray-600", + bgColor: "bg-gray-50", + borderColor: "border-gray-200" + }; + } + }; + + // Fonction pour ouvrir la signature DocuSeal + async function openSignature() { + if (!data) return; + + // Afficher la modale de chargement + setIsLoadingSignature(true); + + let embed: string | null = null; + const title = `Signature (Employeur) · ${data.numero}`; + setModalTitle(title); + + console.log('🔍 [SIGNATURE] Debug - data complète:', data); + console.log('🔍 [SIGNATURE] Debug - data.id:', data.id); + + // Utiliser notre API pour récupérer les données de signature avec service role + try { + const response = await fetch(`/api/contrats/${data.id}/signature`, { + credentials: 'include', + headers: { + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + if (response.status === 404) { + setIsLoadingSignature(false); + toast.error("Ce contrat n'a pas encore de signature électronique configurée. Veuillez d'abord créer la signature via l'interface d'édition du contrat."); + return; + } + throw new Error(`Erreur API: ${response.status}`); + } + + const result = await response.json(); + console.log('📋 [SIGNATURE] Données contrat depuis API:', result); + console.log('🔍 [SIGNATURE] result.data:', result.data); + console.log('🔍 [SIGNATURE] result.data.signature_b64:', result.data?.signature_b64); + + if (!result.success || !result.data) { + setIsLoadingSignature(false); + toast.error("Aucune donnée de signature trouvée pour ce contrat"); + return; + } + + const contractData = result.data; + console.log('📦 [SIGNATURE] contractData extrait:', contractData); + + // Stocker la signature si disponible + const signatureB64 = contractData.signature_b64; + console.log('🖊️ [SIGNATURE] signatureB64 extraite:', { + exists: !!signatureB64, + type: typeof signatureB64, + length: signatureB64?.length, + preview: signatureB64?.substring(0, 50) + }); + + if (signatureB64) { + console.log('✅ [SIGNATURE] Signature B64 disponible, longueur:', signatureB64.length); + } else { + console.log('⚠️ [SIGNATURE] Aucune signature B64 disponible'); + } + + // Vérifier si la soumission DocuSeal n'a pas été créée + if (!contractData.docuseal_submission_id && contractData.signature_status === "Non initiée") { + setIsLoadingSignature(false); + setShowErrorModal(true); + return; + } + + // 1) Si on a un signature_link direct, l'utiliser + if (contractData.signature_link) { + console.log('🔗 [SIGNATURE] Signature link trouvé:', contractData.signature_link); + + // Extraire le docuseal_id du lien de signature + const signatureLinkMatch = contractData.signature_link.match(/docuseal_id=([^&]+)/); + if (signatureLinkMatch) { + const docusealId = signatureLinkMatch[1]; + + // L'URL doit être propre sans paramètres + embed = `https://docuseal.eu/s/${docusealId}`; + console.log('🔗 [SIGNATURE] URL embed depuis signature_link:', embed); + } + } + + // 2) Sinon, récupérer via l'API DocuSeal à partir du template_id + if (!embed && contractData.docuseal_template_id) { + console.log('🔍 [SIGNATURE] Template ID trouvé:', contractData.docuseal_template_id); + + try { + const tId = String(contractData.docuseal_template_id); + + const subRes = await fetch(`/api/docuseal/templates/${encodeURIComponent(tId)}/submissions`, { cache: 'no-store' }); + const subData = await subRes.json(); + + console.log('📋 [SIGNATURE] Submissions DocuSeal:', subData); + + const first = Array.isArray(subData?.data) ? subData.data[0] : (Array.isArray(subData) ? subData[0] : subData); + const subId = first?.id; + + if (subId) { + const detRes = await fetch(`/api/docuseal/submissions/${encodeURIComponent(subId)}`, { cache: 'no-store' }); + const detData = await detRes.json(); + + console.log('📋 [SIGNATURE] Détails submission DocuSeal:', detData); + + const roles = detData?.submitters || detData?.roles || []; + const employer = roles.find((r: any) => (r.role || r.name) === 'Employeur') || {}; + + if (employer?.slug) { + // URL propre sans paramètres + embed = `https://docuseal.eu/s/${employer.slug}`; + console.log('🔗 [SIGNATURE] URL embed depuis DocuSeal API:', embed); + } else { + embed = employer?.embed_src || employer?.sign_src || detData?.embed_src || null; + console.log('🔗 [SIGNATURE] URL embed alternative:', embed); + } + } + } catch (e) { + console.warn('❌ [SIGNATURE] DocuSeal fetch failed', e); + } + } + + if (embed) { + console.log('✅ [SIGNATURE] URL embed trouvée:', embed); + setEmbedSrc(embed); + + // Stocker la signature dans l'etat React pour l'ajouter au composant + console.log('🔍 [SIGNATURE] Stockage de la signature dans l\'etat React...'); + console.log('🔍 [SIGNATURE] signatureB64 value:', signatureB64); + + if (signatureB64) { + console.log('✅ [SIGNATURE] Signature B64 disponible pour pre-remplissage'); + setSignatureB64ForDocuSeal(signatureB64); + } else { + console.log('⚠️ [SIGNATURE] Aucune signature B64 disponible'); + setSignatureB64ForDocuSeal(null); + } + + // Masquer la modale de chargement + setIsLoadingSignature(false); + + // Ouvrir la modale + const dlg = document.getElementById('dlg-signature') as HTMLDialogElement | null; + if (dlg) { + if (typeof dlg.showModal === 'function') { + dlg.showModal(); + } else { + dlg.setAttribute('open', ''); + } + } + } else { + console.warn('❌ [SIGNATURE] Aucune URL d\'embed trouvée'); + setIsLoadingSignature(false); + toast.error("Impossible de récupérer le lien de signature"); + } + } catch (error) { + console.error('❌ [SIGNATURE] Erreur:', error); + setIsLoadingSignature(false); + toast.error("Erreur lors du chargement de la signature"); + } + } + const paies = paiesData?.items ?? []; return ( @@ -600,8 +886,20 @@ export default function ContratMultiPage() { {/* Disposition 2 colonnes (colonnes indépendantes en hauteur) */}
- {/* Colonne gauche : Demande puis Temps de travail réel */} + {/* Colonne gauche : Documents, Demande puis Temps de travail réel */}
+ {/* Card Documents */} + +
- - Télécharger - - ) : ( - Bientôt disponible… - ) - } - /> - - Télécharger - - ) : ( - n/a - ) - } - /> @@ -683,8 +957,83 @@ export default function ContratMultiPage() {
- {/* Colonne droite : Déclarations au-dessus des Paies */} + {/* Colonne droite : Signature électronique, Déclarations, puis Paies */}
+ {/* Card de signature électronique */} + + + +
+ + Signature électronique +
+
+ {(() => { + const IconComponent = getSignatureStatus().icon; + return ; + })()} + {getSignatureStatus().label} +
+
+
+ +
+ {/* Affichage détaillé du statut */} +
+
+ État de l'envoi + { + const etatNormalise = String(data.etat_demande || "").toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, ''); + return etatNormalise.includes("traitee") || etatNormalise.includes("traitée") ? "text-green-600" : "text-gray-600"; + })() + }`}> + {(() => { + const etatNormalise = String(data.etat_demande || "").toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, ''); + return etatNormalise.includes("traitee") || etatNormalise.includes("traitée") ? "Envoyé" : "En cours"; + })()} + +
+
+ Signature employeur + + {data.contrat_signe_employeur === "oui" ? "Oui" : "Non"} + +
+
+ Signature salarié + + {data.contrat_signe_salarie === "oui" ? "Oui" : "Non"} + +
+
+ + {/* Bouton Signer maintenant */} + {(() => { + const etatNormalise = String(data.etat_demande || "").toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, ''); + const isTraitee = etatNormalise.includes("traitee") || etatNormalise.includes("traitée"); + const signatureEmployeurNon = data.contrat_signe_employeur !== "oui"; + + return (isTraitee && signatureEmployeurNon) ? ( +
+ +
+ ) : null; + })()} +
+
+
+
{(() => { const raw = String(data.dpae || ""); @@ -803,19 +1152,12 @@ export default function ContratMultiPage() { # {p.ordre ?? '—'} - {/* Période (Paie traitée) */} - { - // Prefer explicit period_start/period_end from the API when present - (p as any).period_start && (p as any).period_end ? ( - - {formatDateFR((p as any).period_start)} – {formatDateFR((p as any).period_end)} - - ) : p.paie_traitee ? ( - - {formatPeriodDisplay(p.paie_traitee)} - - ) : null - } + {/* Période (format texte) */} + {label && ( + + {label} + + )}
{/* 1. Traitée */} @@ -877,11 +1219,16 @@ export default function ContratMultiPage() { // Récupérer l'URL signée depuis le map const signedUrl = payslipUrlsMap.get(p.id); + const payslipTitle = payLabel(p) || (p.ordre ? `Paie ${p.ordre}` : 'Fiche de paie'); return signedUrl ? ( - + ) : (
{CardInner} @@ -897,6 +1244,114 @@ export default function ContratMultiPage() { {/* Notes */} + {/* Script DocuSeal */} +