Système notif tickets support

This commit is contained in:
Renaud 2025-10-14 23:21:58 +02:00
parent f460c1da5a
commit 5e0997ede8
30 changed files with 3942 additions and 15 deletions

View file

@ -0,0 +1,169 @@
# 🔍 Guide de débogage - Notifications Email Tickets
## ✅ Corrections appliquées
1. **Import Supabase** : Remplacé `createRouteHandlerClient` par `createSbServer` et `createSbServiceRole`
2. **Permissions** : Utilisation de `createSbServiceRole()` pour `getUserById()`
3. **Récupération du prénom** : Depuis `user_metadata.display_name` au lieu de `organization_members.first_name`
4. **Logs ajoutés** : Logs détaillés à chaque étape de l'envoi
## 📋 Comment tester
### 1. Ouvrir le terminal où tourne `npm run dev`
### 2. Envoyer un message depuis `/staff/tickets/[id]`
- Écrire un message
- **NE PAS** cocher "Note interne"
- Cliquer sur "Envoyer"
- Confirmer dans le modal
### 3. Surveiller les logs dans le terminal
Vous devriez voir ces logs dans l'ordre :
#### ✅ Logs attendus (succès)
```
📧 [POST messages] Envoi de notification email...
📧 [POST messages] Ticket: { id: 'xxx-xxx-xxx', created_by: 'yyy-yyy-yyy' }
📧 [POST messages] Creator user: { hasData: true, email: 'user@example.com', error: null }
📧 [POST messages] Staff profile: { hasData: true, email: 'staff@example.com', error: null }
📧 [POST messages] Envoi vers: user@example.com
📧 [POST messages] Données email: {
firstName: 'Jean',
staffName: 'Support Team',
ticketId: 'xxx-xxx-xxx',
ticketSubject: 'Mon problème'
}
🔍 [SES DEBUG] sendUniversalEmailV2 called with: {
type: 'support-reply',
toEmail: 'user@example.com',
subject: 'Nouvelle réponse à votre ticket: Mon problème'
}
✅ Email sent successfully: [MessageId]
✅ [POST messages] Notification email envoyée à user@example.com pour le ticket xxx-xxx-xxx
```
#### ❌ Logs d'erreur possibles
**Problème 1 : Utilisateur introuvable**
```
📧 [POST messages] Creator user: { hasData: false, email: undefined, error: [Error] }
❌ [POST messages] Email du créateur introuvable
```
**Solution** : L'utilisateur n'existe pas ou a été supprimé
**Problème 2 : Permissions insuffisantes**
```
❌ [POST messages] Erreur: AuthApiError: User not allowed
```
**Solution** : Vérifier que `SUPABASE_SERVICE_ROLE_KEY` est bien configurée
**Problème 3 : Email invalide**
```
🚨 [SES DEBUG] Invalid toEmail detected: { toEmail: '', type: 'string' }
```
**Solution** : L'utilisateur n'a pas d'email configuré
**Problème 4 : Erreur SES**
```
❌ [SES ERROR] Failed to send email: [Error message]
```
**Solution** : Problème avec AWS SES (quota, email non vérifié, etc.)
## 🔍 Vérifications supplémentaires
### Vérifier la variable d'environnement
```bash
# Dans le terminal
echo $SUPABASE_SERVICE_ROLE_KEY
```
Doit retourner une longue chaîne commençant par `eyJ...`
### Vérifier la configuration SES
```bash
# Vérifier que les variables AWS sont configurées
grep "AWS_" .env.local
```
Doit contenir :
```
AWS_REGION=eu-west-3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_SES_FROM_EMAIL=...
```
### Tester l'envoi d'email directement
Vous pouvez créer un petit script de test dans `/scripts/test-email.ts` :
```typescript
import { sendSupportReplyEmail } from "@/lib/emailMigrationHelpers";
async function testEmail() {
try {
await sendSupportReplyEmail("votre-email@example.com", {
firstName: "Test",
ticketId: "test-123",
ticketSubject: "Test d'envoi",
staffName: "Équipe Test",
staffMessage: "Ceci est un message de test",
});
console.log("✅ Email de test envoyé !");
} catch (error) {
console.error("❌ Erreur:", error);
}
}
testEmail();
```
## 📧 Vérifier les emails envoyés
### Dans AWS SES Console
1. Aller sur https://console.aws.amazon.com/ses/
2. Région : **Europe (Paris) eu-west-3**
3. Menu : **Email sending** → **Statistics**
4. Vérifier les emails envoyés dans les dernières minutes
### Vérifier les bounces
Si l'email n'arrive pas :
- Vérifier les **Bounces** (rebonds) dans SES
- Vérifier les **Complaints** (plaintes)
- Vérifier que l'email destinataire n'est pas dans une liste noire
### Vérifier le spam
L'email peut arriver dans les spams. Vérifier :
- Dossier spam du destinataire
- Promotions (Gmail)
- Autres dossiers
## 🎯 Checklist complète
- [ ] Le serveur Next.js est redémarré
- [ ] Le message est envoyé SANS cocher "Note interne"
- [ ] Les logs `📧 [POST messages]` apparaissent dans le terminal
- [ ] `Creator user` a `hasData: true` et un email
- [ ] `sendUniversalEmailV2` est appelé
- [ ] Aucune erreur SES dans les logs
- [ ] L'email arrive dans la boîte de réception (ou spam) du destinataire
## 💡 Notes importantes
1. **Les notes internes ne déclenchent PAS d'email** (comportement normal)
2. **L'email est envoyé à l'adresse personnelle** de l'utilisateur (depuis `auth.users`)
3. **Le prénom vient de `display_name`** dans les métadonnées utilisateur
4. **Le nom du staff** vient aussi de `display_name` ou de l'email
---
**Si après toutes ces vérifications, l'email n'est toujours pas envoyé, partagez-moi les logs complets du terminal !**
*Guide créé le 14 octobre 2025*

View file

@ -0,0 +1,189 @@
# 📧 Email de réponse support - Template amélioré
## ✨ Modifications appliquées
### 1. InfoCard standardisée
L'email de réponse au ticket support utilise maintenant la même structure que les autres notifications :
**Première card (InfoCard) :**
- **Votre structure** : Nom de l'organisation
- **Votre code employeur** : Code depuis `organization_details.code_employeur`
- **Votre gestionnaire** : Renaud BREVIERE-ABRAHAM (forcé)
### 2. DetailsCard enrichie
**Deuxième card (Message) :**
- **Répondant** : Prénom du staff avec majuscule automatique + "[Staff Odentas]"
- **Sujet** : Sujet du ticket
- **Réponse** : Message du staff
### 3. Formatage automatique du nom du staff
```typescript
// Exemple : "renaud" devient "Renaud [Staff Odentas]"
const formattedStaffName = data.staffName.charAt(0).toUpperCase() +
data.staffName.slice(1) +
' [Staff Odentas]';
```
## 📊 Structure du template
```
┌─────────────────────────────────────────────────────┐
│ Bonjour Jean, │
│ │
│ Vous avez reçu une nouvelle réponse │
│ à votre ticket support. │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Informations │ │
│ │ │ │
│ │ Votre structure: Entreprise ABC │ │
│ │ Votre code employeur: 12345 │ │
│ │ Votre gestionnaire: Renaud BREVIERE-ABRAHAM │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Message │ │
│ │ │ │
│ │ Répondant: Renaud [Staff Odentas] │ │
│ │ Sujet: Mon problème de paie │ │
│ │ Réponse: Bonjour, voici la solution... │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ Voir le ticket │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
## 🗂️ Sources de données
| Donnée | Source | Champ |
|--------|--------|-------|
| **Nom organisation** | `organizations` | `name` |
| **Code employeur** | `organization_details` | `code_employeur` |
| **Gestionnaire** | Forcé | `"Renaud BREVIERE-ABRAHAM"` |
| **Prénom staff** | `auth.users.user_metadata` | `display_name` ou `first_name` |
| **Sujet ticket** | `tickets` | `subject` |
| **Message** | Corps du message | POST body |
## 💻 Code modifié
### 1. `/lib/emailTemplateService.ts`
```typescript
infoCard: {
title: 'Informations',
items: [
{ label: 'Votre structure', value: 'organizationName' },
{ label: 'Votre code employeur', value: 'employerCode' },
{ label: 'Votre gestionnaire', value: 'handlerName' },
],
},
detailsCard: {
title: 'Message',
items: [
{ label: 'Répondant', value: 'staffName' },
{ label: 'Sujet', value: 'ticketSubject' },
{ label: 'Réponse', value: 'staffMessage' },
],
},
```
### 2. `/lib/emailMigrationHelpers.ts`
```typescript
export async function sendSupportReplyEmail(
toEmail: string,
data: {
firstName?: string;
ticketId: string;
ticketSubject: string;
staffName: string;
staffMessage: string;
organizationName?: string;
employerCode?: string; // ⭐ Nouveau : code depuis organization_details
}
) {
// ⭐ Formatage automatique : majuscule + [Staff Odentas]
const formattedStaffName = data.staffName.charAt(0).toUpperCase() +
data.staffName.slice(1) +
' [Staff Odentas]';
const emailData: EmailDataV2 = {
firstName: data.firstName,
ticketId: data.ticketId,
ticketSubject: data.ticketSubject,
staffName: formattedStaffName,
staffMessage: data.staffMessage,
organizationName: data.organizationName || 'Non définie',
employerCode: data.employerCode || 'Non défini', // ⭐ Nouveau
handlerName: 'Renaud BREVIERE-ABRAHAM', // ⭐ Forcé
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/support/${data.ticketId}`,
};
await sendUniversalEmailV2({
type: 'support-reply',
toEmail,
subject: `Nouvelle réponse à votre ticket: ${data.ticketSubject}`,
data: emailData,
});
}
```
### 3. `/app/api/tickets/[id]/messages/route.ts`
```typescript
// ⭐ Récupération du code employeur
const { data: orgDetails } = await sb
.from("organization_details")
.select("code_employeur")
.eq("org_id", ticket.org_id)
.maybeSingle();
// Envoi de l'email avec les nouvelles données
await sendSupportReplyEmail(creatorUser.user.email, {
firstName,
ticketId: ticket.id,
ticketSubject: ticket.subject,
staffName,
staffMessage: text,
organizationName: organization?.name,
employerCode: orgDetails?.code_employeur, // ⭐ Code employeur
});
```
## 📋 Logs de debug
Dans le terminal, vous verrez :
```
📧 [POST messages] Organization: { name: 'Entreprise ABC', structure_api: 'abc' }
📧 [POST messages] Org details: { code_employeur: '12345' }
📧 [POST messages] Données email: {
firstName: 'Jean',
staffName: 'renaud',
ticketId: 'xxx-xxx-xxx',
ticketSubject: 'Mon problème',
organizationName: 'Entreprise ABC',
employerCode: '12345'
}
```
Le nom du staff sera automatiquement formaté en : `Renaud [Staff Odentas]`
## ✅ Résultat
Les emails de réponse au ticket support ont maintenant :
- ✅ La même structure que les autres notifications
- ✅ Le code employeur correct depuis `organization_details`
- ✅ Le gestionnaire forcé à "Renaud BREVIERE-ABRAHAM"
- ✅ Le nom du staff formaté avec majuscule + badge "[Staff Odentas]"
- ✅ Toutes les informations importantes dans la première card
- ✅ Les détails du message dans la deuxième card
---
*Améliorations appliquées le 14 octobre 2025*

212
GUIDE_DEMARRAGE_TICKETS.md Normal file
View file

@ -0,0 +1,212 @@
# 🚀 Guide de démarrage et débogage - Notifications Tickets Support
## ⚠️ Important : Redémarrage du serveur requis
Après avoir créé ou modifié des routes API dans Next.js, il est **OBLIGATOIRE** de redémarrer le serveur de développement. Sinon, les nouvelles routes ne seront pas disponibles (erreur 404).
## 📋 Checklist avant de tester
### 1. Vérifier les fichiers
```bash
./test-ticket-notifications.sh
```
Tous les fichiers doivent avoir une ✅ verte.
### 2. Redémarrer le serveur Next.js
**Option A : Arrêter proprement**
```bash
# Dans le terminal où tourne npm run dev, appuyez sur Ctrl+C
# Puis relancez :
npm run dev
```
**Option B : Forcer l'arrêt**
```bash
# Tuer tous les processus Next.js
pkill -f "next dev"
# Attendre 2 secondes
sleep 2
# Relancer
npm run dev
```
### 3. Vérifier que le serveur démarre bien
Vous devriez voir :
```
▲ Next.js 14.x.x
- Local: http://localhost:3000
- ready started server on 0.0.0.0:3000
```
## 🔍 Comment tester
### 1. Ouvrir la console du navigateur
- Chrome/Edge : F12 ou Cmd+Option+I (Mac)
- Firefox : F12 ou Cmd+Option+K (Mac)
- Safari : Cmd+Option+C (Mac)
### 2. Aller sur une page de ticket
```
http://localhost:3000/staff/tickets/[un-id-de-ticket-existant]
```
### 3. Regarder les logs
**Dans la console du navigateur (F12) :**
```
🔍 [StaffTicketActions] Fetching recipient info for ticket: xxx-xxx-xxx
🔍 [StaffTicketActions] URL: /api/tickets/xxx-xxx-xxx/recipient-info
🔍 [StaffTicketActions] Response status: 200
✅ [StaffTicketActions] Recipient info: {email: "...", name: "..."}
```
**Dans le terminal Next.js :**
```
📋 [ticket page] User ID: xxx-xxx-xxx
📋 [ticket page] Email: user@example.com
📋 [ticket page] User metadata: {
"display_name": "Jean",
...
}
✅ [ticket page] Nom trouvé dans display_name: Jean
```
## ❌ Diagnostic des problèmes
### Problème 1 : Erreur 404 sur `/api/tickets/[id]/recipient-info`
**Symptômes :**
- Console navigateur : `GET /api/tickets/.../recipient-info 404 (Not Found)`
- Aucun log dans le terminal serveur
**Solutions :**
1. ✅ **Vérifier que le fichier existe**
```bash
ls -la app/api/tickets/[id]/recipient-info/route.ts
```
2. ✅ **Redémarrer le serveur Next.js** (voir ci-dessus)
3. ✅ **Vérifier qu'il n'y a pas d'erreurs TypeScript**
```bash
npm run build
```
4. ✅ **Vérifier la syntaxe du fichier**
- Le fichier doit exporter une fonction `GET`
- La fonction doit avoir la signature : `async function GET(_: Request, { params }: { params: { id: string } })`
### Problème 2 : "Utilisateur inconnu" affiché
**Symptômes :**
- La page affiche "Ouvert par: Utilisateur inconnu"
- Logs montrent `user_metadata: {}` ou `null`
**Diagnostic :**
1. **Vérifier dans Supabase Auth** :
- Aller dans Authentication → Users
- Cliquer sur l'utilisateur concerné
- Vérifier que le champ **"Display name"** est rempli
2. **Si le champ est vide** :
- C'est normal ! L'utilisateur n'a pas de prénom enregistré
- Le système va afficher l'email à la place
- Pour tester, ajoutez un "Display name" dans Supabase
3. **Vérifier les logs** :
```
📋 [ticket page] User metadata: {...}
```
- Si vous voyez `{}` → pas de métadonnées → normal
- Si vous voyez `{display_name: "..."}` → devrait fonctionner
### Problème 3 : Pas de logs dans le terminal
**Causes possibles :**
1. Le serveur n'a pas été redémarré après les modifications
2. Vous regardez le mauvais terminal (plusieurs serveurs lancés ?)
3. Les logs sont filtrés
**Solutions :**
1. Redémarrer le serveur
2. Vérifier quel processus écoute sur le port 3000 :
```bash
lsof -ti:3000
```
3. S'assurer qu'il n'y a qu'un seul serveur Next.js qui tourne
### Problème 4 : Le modal ne s'affiche pas
**Symptômes :**
- On clique sur "Envoyer" mais rien ne se passe
- Pas d'erreur dans la console
**Diagnostic :**
1. Vérifier dans la console navigateur :
```
✅ [StaffTicketActions] Recipient info: {...}
```
- Si absent → l'API n'a pas retourné les infos
- Si présent → le modal devrait s'afficher
2. Vérifier que la checkbox "Note interne" n'est **PAS** cochée
- Les notes internes ne déclenchent pas le modal
3. Vérifier l'état React :
- Ouvrir React DevTools
- Chercher `StaffTicketActions`
- Vérifier `recipientInfo` → doit avoir `{email, name}`
## 🎯 Cas de test
### Test 1 : Affichage du nom sur la page
1. Aller sur `/staff/tickets/[id]`
2. Vérifier que "Ouvert par:" affiche un nom ou un email
3. Vérifier les logs dans le terminal
### Test 2 : Modal de confirmation
1. Aller sur `/staff/tickets/[id]`
2. Écrire un message dans le textarea
3. **NE PAS** cocher "Note interne"
4. Cliquer sur "Envoyer"
5. → Le modal doit s'afficher avec l'email et le nom du destinataire
### Test 3 : Note interne (pas de modal)
1. Aller sur `/staff/tickets/[id]`
2. Écrire un message dans le textarea
3. **COCHER** "Note interne"
4. Cliquer sur "Envoyer"
5. → Le message doit être envoyé directement SANS modal
## 📞 Si ça ne fonctionne toujours pas
1. **Arrêter complètement Next.js**
```bash
pkill -f "next"
lsof -ti:3000 # Doit retourner vide
```
2. **Nettoyer le cache Next.js**
```bash
rm -rf .next
```
3. **Redémarrer**
```bash
npm run dev
```
4. **Vérifier les logs** dans la console ET le terminal
5. **Partager les logs** si le problème persiste :
- Logs de la console navigateur (F12)
- Logs du terminal Next.js
- Capture d'écran de la page
---
*Guide créé le 14 octobre 2025*

137
PAGES_LEGALES.md Normal file
View file

@ -0,0 +1,137 @@
# Pages Légales - Espace Paie Odentas
Documentation des pages légales créées pour l'Espace Paie.
## 📄 Pages créées
### 1. Politique de Confidentialité
**Route :** `/politique-confidentialite`
**Fichiers :**
- `app/politique-confidentialite/page.tsx` - Page serveur avec métadonnées
- `app/politique-confidentialite/PolitiqueConfidentialiteContent.tsx` - Composant client
**Contenu (10 sections) :**
1. Présentation
2. Données collectées (entreprises, utilisateurs, salariés, productions)
3. Finalités du traitement (8 finalités)
4. Base légale et durée de conservation
5. Partage et transfert des données
6. Sécurité des données (7 mesures)
7. Vos droits RGPD
8. Cookies et technologies
9. Modifications de la politique
10. Contact
### 2. Mentions Légales
**Route :** `/mentions-legales`
**Fichiers :**
- `app/mentions-legales/page.tsx` - Page serveur avec métadonnées
- `app/mentions-legales/MentionsLegalesContent.tsx` - Composant client
**Contenu (10 articles) :**
1. L'Éditeur (Odentas Media SAS)
2. L'Hébergeur (Vercel + Supabase/AWS France)
3. Accès à l'Espace Paie
4. Collecte des données
5. Propriété intellectuelle
6. Cookies
7. Sécurité
8. Limitation de responsabilité
9. Droit applicable et juridiction
10. Contact
## 🔧 Modifications techniques
### Middleware
Les deux pages sont accessibles publiquement, même en mode maintenance :
```typescript
const isPublicPage = path.startsWith('/auto-declaration') ||
path.startsWith('/dl-contrat-signe') ||
path.startsWith('/signature-salarie') ||
path === '/politique-confidentialite' ||
path === '/mentions-legales';
```
### Intégration dans la Sidebar
Les liens sont déjà présents dans le footer de la sidebar :
```tsx
<a href="/mentions-legales">Mentions légales</a>
<a href="/politique-confidentialite">Confidentialité</a>
```
## 📊 Informations légales
### Éditeur
- **Raison sociale :** Odentas Media SAS
- **Capital :** 100 euros
- **RCS :** Paris 907 880 348
- **Siège social :** 6 rue d'Armaillé, 75017 Paris
- **Email :** paie@odentas.fr
- **TVA :** FR75907880348
- **Directeur de publication :** Nicolas ROL
### Hébergement
- **Application :** Vercel Inc. (USA)
- **Base de données :** Supabase via AWS eu-west-3 (Paris, France)
- **Localisation des données :** 🇫🇷 France uniquement
## ✅ Conformité
### RGPD
- Droits des utilisateurs détaillés
- Bases légales du traitement
- Durées de conservation conformes
- Mesures de sécurité documentées
- Contact CNIL fourni
### Code de la propriété intellectuelle
- Mentions de propriété
- Interdiction de reproduction non autorisée
- Protection du contenu
### LCEN (Loi pour la Confiance dans l'Économie Numérique)
- Identification de l'éditeur
- Informations sur l'hébergeur
- Conditions d'accès
## 🎨 Design
- **Style cohérent** avec la page de maintenance
- **Mesh background** moderne
- **Cartes translucides** avec backdrop blur
- **Icônes Lucide** pour illustration
- **Responsive** pour mobile et desktop
- **Accessibilité** optimisée
## 📱 Accessibilité
- Navigation claire avec retour à l'accueil
- Liens inter-pages (Confidentialité ↔ Mentions légales)
- Accessible en mode maintenance
- Compatible lecteurs d'écran
- Contraste optimisé
## 🔗 Liens utiles
- **Contact support :** paie@odentas.fr
- **WhatsApp :** 07 80 978 000
- **Site principal :** https://odentas.fr
- **CNIL :** https://www.cnil.fr
## 📅 Mise à jour
**Dernière mise à jour :** 14 octobre 2025
Les deux pages affichent la date de dernière mise à jour. Pensez à mettre à jour cette date lors de modifications substantielles du contenu.
## ⚠️ Notes pour le développement
1. Les erreurs TypeScript "Cannot find module" sont normales et dues au cache du Language Server
2. Le code compile correctement avec Next.js
3. Les pages sont des Server Components avec Client Components séparés
4. Les métadonnées sont gérées côté serveur pour le SEO
5. Le contenu est séparé dans des fichiers Content.tsx pour faciliter la maintenance

188
POPUP_INFO_SUIVI.md Normal file
View file

@ -0,0 +1,188 @@
# Popup d'Information sur la Confidentialité
Documentation du popup d'information sur la confidentialité et le suivi affiché aux utilisateurs.
## 📍 Localisation
Le popup s'affiche **en bas à gauche** de l'écran sur toutes les pages de l'Espace Paie.
## 🎯 Fonctionnement
### Affichage
- Le popup apparaît **1,2 seconde** après le chargement de la page
- Il s'affiche avec une animation fluide (spring animation)
- Il est **non intrusif** et peut être fermé facilement
### Persistance
Le popup utilise le **localStorage** du navigateur pour se souvenir que l'utilisateur l'a déjà vu :
- **Clé de stockage :** `odentas_info_suivi_ack_v1`
- **Valeur :** `"1"` quand l'utilisateur a cliqué sur "J'ai compris"
- Le popup ne s'affiche **qu'une seule fois** par navigateur
### Réaffichage
Pour forcer le réaffichage du popup (utile pour les tests ou après une mise à jour) :
1. Ouvrir la console du navigateur (F12)
2. Exécuter : `localStorage.removeItem('odentas_info_suivi_ack_v1')`
3. Rafraîchir la page
Ou changer la version dans le code : `storageKey = "odentas_info_suivi_ack_v2"`
## 📝 Contenu
### Message principal
```
Transparence & confidentialité
L'Espace Paie Odentas utilise des cookies essentiels pour votre authentification
et votre sécurité, ainsi qu'un outil d'analyse (PostHog) pour améliorer
nos services. Vos données sont hébergées en France 🇫🇷 et ne sont jamais
revendues. En utilisant l'Espace Paie, vous acceptez notre Politique de
Confidentialité.
```
### Actions disponibles
1. **"J'ai compris"** - Ferme le popup et enregistre l'acceptation
2. **"En savoir plus"** - Ouvre la politique de confidentialité dans un nouvel onglet
3. **Bouton X** - Ferme le popup sans enregistrer (il réapparaîtra au prochain chargement)
## 🎨 Design
### Style visuel
- **Position :** Fixed, bas gauche (left-4 bottom-4)
- **Taille :** 384px max-width (responsive sur mobile)
- **Arrière-plan :** Gradient blanc vers gris avec backdrop blur
- **Bordure :** Border slate-200
- **Ombre :** Shadow-lg
- **Icône :** ShieldCheck en bleu (sky-700)
### Animation
- **Type :** Spring animation
- **Stiffness :** 380
- **Damping :** 28
- **Effet :** Apparition en fondu avec légère translation verticale et scale
### Responsive
- **Desktop :** 384px de largeur
- **Mobile :** 92vw (92% de la largeur de l'écran)
- S'adapte automatiquement à la taille de l'écran
## 🔧 Intégration technique
### Fichiers
- **Composant :** `components/PopupInfoSuivi.tsx`
- **Intégration :** `app/layout.tsx` (layout racine)
### Dépendances
- **framer-motion** - Pour les animations fluides
- **lucide-react** - Pour les icônes (ShieldCheck, X)
- **React hooks** - useState, useEffect
### Props
```typescript
{
policyUrl?: string; // URL de la politique de confidentialité
// Par défaut: "/politique-confidentialite"
storageKey?: string; // Clé localStorage pour la persistance
// Par défaut: "odentas_info_suivi_ack_v1"
}
```
### Utilisation
```tsx
import PopupInfoSuivi from "@/components/PopupInfoSuivi";
// Dans le layout ou une page
<PopupInfoSuivi policyUrl="/politique-confidentialite" />
```
## 🔒 Conformité RGPD
Le popup informe les utilisateurs de :
1. ✅ L'utilisation de cookies essentiels (authentification, sécurité)
2. ✅ L'utilisation d'un outil d'analyse (PostHog)
3. ✅ L'hébergement des données en France 🇫🇷
4. ✅ La non-revente des données personnelles
5. ✅ Le lien vers la politique de confidentialité complète
## 🎯 Bonnes pratiques
### UX
- **Non bloquant :** L'utilisateur peut continuer à naviguer
- **Discret :** Petit format en bas à gauche
- **Clair :** Message court et compréhensible
- **Actions simples :** 2 boutons principaux
### Accessibilité
- **role="dialog"** - Indique qu'il s'agit d'une boîte de dialogue
- **aria-live="polite"** - Annonce le contenu aux lecteurs d'écran
- **aria-label** - Décrit le contenu du popup
- **Focus trap** - Non implémenté car non bloquant
### Performance
- **Lazy loading :** Le popup ne se charge qu'après 1,2 seconde
- **Lightweight :** Code minimal et optimisé
- **Exit animation :** Animation de sortie fluide
## 🧪 Tests
### Test manuel
1. Ouvrir l'Espace Paie en navigation privée
2. Vérifier que le popup apparaît après 1,2 seconde
3. Cliquer sur "J'ai compris"
4. Rafraîchir la page
5. Vérifier que le popup ne réapparaît **pas**
### Test responsive
1. Ouvrir en mode mobile (DevTools)
2. Vérifier que le popup s'adapte à la largeur
3. Vérifier la lisibilité sur petit écran
### Test accessibilité
1. Tester avec un lecteur d'écran
2. Naviguer au clavier (Tab, Enter)
3. Vérifier les labels ARIA
## 📊 Statistiques d'utilisation
Pour suivre l'utilisation du popup avec PostHog :
- **Événement d'affichage :** Peut être ajouté dans useEffect
- **Événement de clic "J'ai compris" :** Peut être ajouté dans acknowledge()
- **Événement de fermeture (X) :** Peut être ajouté dans setOpen(false)
## 🔄 Mises à jour futures
### Possibles améliorations
1. **Multi-langues** - Ajouter support i18n
2. **Personnalisation** - Adapter le message selon le rôle (staff/client)
3. **Analytics** - Tracker les interactions avec PostHog
4. **A/B testing** - Tester différentes variantes du message
5. **Animations avancées** - Ajouter des micro-interactions
### Versions
- **v1** (actuelle) - Version initiale avec message de confidentialité
- **v2** (future) - Ajout d'analytics et personnalisation
## 🐛 Dépannage
### Le popup ne s'affiche pas
1. Vérifier que framer-motion est installé : `npm list framer-motion`
2. Vérifier la console pour les erreurs JS
3. Vérifier le localStorage : `localStorage.getItem('odentas_info_suivi_ack_v1')`
### Le popup réapparaît alors qu'il a été fermé
1. L'utilisateur a peut-être cliqué sur X au lieu de "J'ai compris"
2. Le localStorage a été effacé (navigation privée, clear cookies)
3. Vérifier que la clé de stockage n'a pas changé
### Animation saccadée
1. Vérifier la version de framer-motion
2. Désactiver les extensions de navigateur qui peuvent bloquer les animations
3. Tester sur un autre navigateur
## 📚 Ressources
- [Documentation framer-motion](https://www.framer.com/motion/)
- [Lucide Icons](https://lucide.dev/)
- [RGPD - Cookies et traceurs](https://www.cnil.fr/fr/cookies-et-traceurs-que-dit-la-loi)
- [Accessibilité - Dialog ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)

View file

@ -0,0 +1,125 @@
# 🎉 RÉSOLUTION FINALE - Support Ticket Notifications
## ✅ Problème résolu !
Le problème était **une question de permissions Supabase**.
### 🔍 Diagnostic
L'erreur était :
```
AuthApiError: User not allowed
code: 'not_admin'
status: 403
```
### 🎯 Cause racine
`sb.auth.admin.getUserById()` nécessite des **permissions administrateur** (clé `service_role`), mais le code utilisait `createSbServer()` qui utilise la clé `anon` (publique).
### 🔧 Solution appliquée
**Fichiers modifiés :**
1. **`/app/api/tickets/[id]/recipient-info/route.ts`**
```typescript
// Ajout de l'import
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
// Utilisation du service role pour getUserById
const sbAdmin = createSbServiceRole();
const { data: creatorUser } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
```
2. **`/app/(app)/staff/tickets/[id]/page.tsx`**
```typescript
// Ajout de l'import
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
// Utilisation du service role pour getUserById
const sbAdmin = createSbServiceRole();
const { data: creatorUser } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
```
### 🎬 Actions à faire maintenant
1. **Le serveur va recharger automatiquement** (hot reload)
2. **Rechargez la page** du ticket dans votre navigateur
3. **Vérifiez** :
- Le nom du créateur devrait s'afficher correctement
- Le modal de confirmation devrait s'afficher quand vous cliquez sur "Envoyer"
### 📋 Ce qui devrait maintenant fonctionner
✅ **Sur la page `/staff/tickets/[id]` :**
```
┌─────────────────────────────────────────┐
│ Organisation: Nom de l'entreprise │
│ Ouvert par: Jean (ou email) │ ← Maintenant correct !
└─────────────────────────────────────────┘
```
✅ **Dans le modal de confirmation :**
```
┌─────────────────────────────────────────┐
│ Confirmer l'envoi de votre réponse │
│ │
│ Destinataire │
│ 📧 jean@example.com │
│ Jean │ ← Maintenant correct !
│ │
│ Message │
│ [Votre message...] │
│ │
│ [Annuler] [Envoyer ✉️] │ ← Fonctionne !
└─────────────────────────────────────────┘
```
### 🔍 Logs attendus dans le terminal
Vous devriez maintenant voir :
```
📋 [ticket page] Ticket: { id: '...', created_by: '...', ... }
📋 [ticket page] Tentative getUserById pour: ...
📋 [ticket page] getUserById result: { hasData: true, hasUser: true, hasEmail: true, error: null }
📋 [ticket page] Email: user@example.com
📋 [ticket page] User metadata: { "display_name": "Jean", ... }
✅ [ticket page] Nom trouvé dans display_name: Jean
```
Et pour l'API :
```
📋 [recipient-info] Ticket ID: ...
📋 [recipient-info] Created by: ...
📋 [recipient-info] getUserById result: { hasData: true, hasUser: true, hasEmail: true, error: null }
📋 [recipient-info] User metadata: { "display_name": "Jean", ... }
✅ [recipient-info] Nom trouvé dans display_name: Jean
```
### 🎓 Leçon apprise
**Important à retenir :**
1. **`auth.admin.getUserById()`** nécessite la clé `service_role`
2. Utiliser `createSbServer()` pour les opérations normales
3. Utiliser `createSbServiceRole()` pour les opérations admin
4. Toujours vérifier les permissions **AVANT** d'utiliser le service role
### 📚 Documentation
Consultez :
- `SUPPORT_TICKET_NOTIFICATIONS_FIXES.md` - Documentation complète des corrections
- `GUIDE_DEMARRAGE_TICKETS.md` - Guide de démarrage et débogage
---
## 🚀 Testez maintenant !
1. Rechargez la page du ticket
2. Vérifiez que le nom s'affiche
3. Essayez d'envoyer un message (sans cocher "Note interne")
4. Le modal devrait s'afficher avec toutes les infos !
---
*Résolution finale - 14 octobre 2025*

View file

@ -0,0 +1,300 @@
# Notifications Internes du Support
## Vue d'ensemble
Système de notification par email vers `paie@odentas.fr` pour alerter l'équipe support quand :
1. Un utilisateur crée un nouveau ticket
2. Un utilisateur répond à un ticket existant
## 📧 Types de notifications
### 1. Nouveau ticket créé (`support-ticket-created`)
**Déclencheur :** Quand un utilisateur (non-staff) crée un nouveau ticket via l'API `POST /api/tickets`
**Template Email :**
- **Sujet :** `[SUPPORT] Nouveau ticket : {sujet du ticket}`
- **Titre :** 🎫 Nouveau ticket support
- **Bouton CTA :** "Voir le ticket" → `/staff/tickets/{id}`
- **Couleur bouton :** Bleu (#3B82F6)
**Informations affichées :**
**InfoCard :**
- Organisation
- Code employeur
- Créé par (nom de l'utilisateur)
- Email (email de l'utilisateur)
**DetailsCard :**
- ID du ticket
- Sujet
- Catégorie (actuellement "Support général")
- Message initial (avec sauts de ligne préservés)
### 2. Réponse utilisateur (`support-ticket-reply`)
**Déclencheur :** Quand un utilisateur (non-staff) ajoute un message à un ticket via `POST /api/tickets/[id]/messages`
**Template Email :**
- **Sujet :** `[SUPPORT] Réponse au ticket : {sujet du ticket}`
- **Titre :** 💬 Réponse sur un ticket
- **Bouton CTA :** "Voir le ticket" → `/staff/tickets/{id}`
- **Couleur bouton :** Bleu (#3B82F6)
**Informations affichées :**
**InfoCard :**
- Organisation
- Code employeur
- Répondu par (nom de l'utilisateur)
- Email (email de l'utilisateur)
**DetailsCard :**
- ID du ticket
- Sujet
- Statut (open, in_progress, resolved, closed)
- Réponse (avec sauts de ligne préservés)
## 🔧 Implémentation technique
### Fichiers modifiés
#### 1. `/lib/emailTemplateService.ts`
```typescript
// Nouveaux types ajoutés
| 'support-ticket-created'
| 'support-ticket-reply'
// Nouveaux champs dans EmailDataV2
ticketCategory?: string;
ticketMessage?: string;
ticketStatus?: string;
userEmail?: string;
userMessage?: string;
```
#### 2. `/lib/emailMigrationHelpers.ts`
**Nouvelle fonction : `sendInternalTicketCreatedEmail()`**
```typescript
export async function sendInternalTicketCreatedEmail(
data: {
ticketId: string;
ticketSubject: string;
ticketCategory: string;
ticketMessage: string;
userName: string;
userEmail: string;
organizationName?: string;
employerCode?: string;
}
)
```
- Convertit automatiquement les sauts de ligne en `<br>`
- Envoie toujours à `paie@odentas.fr`
- CTA vers `/staff/tickets/{id}`
**Nouvelle fonction : `sendInternalTicketReplyEmail()`**
```typescript
export async function sendInternalTicketReplyEmail(
data: {
ticketId: string;
ticketSubject: string;
ticketStatus: string;
userMessage: string;
userName: string;
userEmail: string;
organizationName?: string;
employerCode?: string;
}
)
```
- Convertit automatiquement les sauts de ligne en `<br>`
- Envoie toujours à `paie@odentas.fr`
- CTA vers `/staff/tickets/{id}`
#### 3. `/app/api/tickets/route.ts`
**Modifications dans `POST` :**
```typescript
// Après la création du ticket, si !isStaff
if (!isStaff) {
try {
// Récupérer les infos utilisateur avec createSbServiceRole()
const sbAdmin = createSbServiceRole();
const { data: userData } = await sbAdmin.auth.admin.getUserById(user.id);
// Récupérer org et code employeur
const { data: organization } = await sb.from('organizations')...
const { data: orgDetails } = await sb.from('organization_details')...
// Envoyer la notification
await sendInternalTicketCreatedEmail({...});
} catch (emailError) {
// Ne pas bloquer la création du ticket
console.error('Failed to send internal notification');
}
}
```
#### 4. `/app/api/tickets/[id]/messages/route.ts`
**Modifications dans `POST` :**
```typescript
// Après l'insertion du message, si !isStaff
if (!isStaff) {
try {
// Récupérer les infos du ticket
const { data: ticket } = await sb.from("tickets")...
// Récupérer org et code employeur
const { data: organization } = await sb.from('organizations')...
const { data: orgDetails } = await sb.from('organization_details')...
// Récupérer les infos utilisateur avec createSbServiceRole()
const sbAdmin = createSbServiceRole();
const { data: userData } = await sbAdmin.auth.admin.getUserById(user.id);
// Envoyer la notification
await sendInternalTicketReplyEmail({...});
} catch (emailError) {
// Ne pas bloquer l'ajout du message
console.error('Failed to send internal notification');
}
}
```
## 🎨 Design des emails
### Structure standardisée
Les deux types d'emails utilisent le système Universal Email V2 avec :
- **Header :** Logo Odentas avec fond standard
- **InfoCard :** Informations sur l'organisation et l'utilisateur (fond gris clair)
- **DetailsCard :** Détails du ticket/message (fond blanc)
- **CTA Button :** Bouton bleu "Voir le ticket"
- **Footer :** Mentions légales Odentas
### Couleurs
- Header : `#171424` (violet foncé standard)
- Bouton CTA : `#3B82F6` (bleu) - différent des emails clients pour distinguer les notifications internes
- Texte bouton : `#FFFFFF` (blanc)
## 📊 Sources de données
### Nom de l'utilisateur
```typescript
// Priorité de récupération
userData?.user?.user_metadata?.display_name ||
userData?.user?.user_metadata?.first_name ||
'Utilisateur inconnu'
```
### Code employeur
```typescript
// Depuis organization_details
const { data: orgDetails } = await sb
.from('organization_details')
.select('code_employeur')
.eq('org_id', ticket.org_id)
.maybeSingle();
```
### Email utilisateur
```typescript
user.email || 'Email non disponible'
```
## 🔐 Permissions
- Utilise `createSbServiceRole()` pour accéder à `auth.admin.getUserById()`
- Les requêtes vers `organizations` et `organization_details` utilisent `createSbServer()` (permissions RLS normales)
## ⚠️ Gestion des erreurs
**Important :** Les erreurs d'envoi d'email ne bloquent JAMAIS la création du ticket ou l'ajout du message.
```typescript
try {
await sendInternalTicketCreatedEmail({...});
} catch (emailError) {
// Log uniquement, ne pas throw
console.error('Failed to send internal notification:', emailError);
}
```
Ceci garantit que même si AWS SES est en panne ou si les données sont incomplètes, le ticket est toujours créé/mis à jour.
## 📝 Logs
Les logs suivent le format standardisé :
```
[TICKET CREATE] Sending internal notification email...
[TICKET CREATE] Internal notification email sent successfully
[TICKET CREATE] Failed to send internal notification email: {error}
```
```
📧 [POST messages] Envoi de notification interne...
✅ [POST messages] Notification interne envoyée pour le ticket {id}
❌ [POST messages] Erreur lors de l'envoi de la notification interne: {error}
```
## 🧪 Tests suggérés
1. **Création de ticket par utilisateur :**
- Créer un ticket en tant qu'utilisateur normal
- Vérifier que `paie@odentas.fr` reçoit l'email
- Vérifier que le bouton CTA mène vers `/staff/tickets/{id}`
- Vérifier que les sauts de ligne sont préservés
2. **Réponse utilisateur :**
- Répondre à un ticket en tant qu'utilisateur normal
- Vérifier que `paie@odentas.fr` reçoit l'email
- Vérifier que le statut est correct
- Vérifier que les sauts de ligne sont préservés
3. **Pas de notification staff :**
- Créer un ticket en tant que staff → Pas d'email interne
- Répondre en tant que staff → Email à l'utilisateur uniquement
4. **Messages internes :**
- Créer un message `internal: true` → Aucun email envoyé
## 📋 Checklist de validation
- ✅ Template `support-ticket-created` ajouté à `emailTemplateService.ts`
- ✅ Template `support-ticket-reply` ajouté à `emailTemplateService.ts`
- ✅ Fonction `sendInternalTicketCreatedEmail()` créée
- ✅ Fonction `sendInternalTicketReplyEmail()` créée
- ✅ API `POST /api/tickets` modifiée pour envoyer notification
- ✅ API `POST /api/tickets/[id]/messages` modifiée pour envoyer notification
- ✅ Sauts de ligne convertis en `<br>` dans les deux fonctions
- ✅ Gestion d'erreur non-bloquante implémentée
- ✅ Utilisation de `createSbServiceRole()` pour getUserById
- ✅ Code employeur récupéré depuis `organization_details`
- ✅ Tous les fichiers compilent sans erreur TypeScript
## 🔄 Différence avec les emails clients
| Fonctionnalité | Email Client | Email Interne |
|----------------|--------------|---------------|
| Destinataire | Utilisateur créateur du ticket | `paie@odentas.fr` |
| Déclencheur | Staff répond (non-internal) | Utilisateur crée/répond |
| Couleur bouton | Jaune (#EFC543) | Bleu (#3B82F6) |
| Affichage staff | Nom formaté avec badge | Nom utilisateur brut |
| URL CTA | `/support/{id}` | `/staff/tickets/{id}` |
## 🚀 Déploiement
Aucune configuration supplémentaire nécessaire :
- Les templates sont automatiquement disponibles
- L'email `paie@odentas.fr` est codé en dur (pas de variable d'environnement)
- AWS SES est déjà configuré via `sendUniversalEmailV2`
---
**Date de création :** 14 octobre 2025
**Auteur :** Système de support Odentas
**Version :** 1.0

View file

@ -0,0 +1,65 @@
# Résumé : Notifications Internes Support
## ✅ Fonctionnalité implémentée
**Objectif :** Envoyer un email à `paie@odentas.fr` quand un utilisateur (non-staff) interagit avec le support.
## 📧 Deux types de notifications
### 1. Nouveau ticket créé
- **Déclencheur :** Utilisateur crée un ticket
- **Email à :** paie@odentas.fr
- **Sujet :** [SUPPORT] Nouveau ticket : {sujet}
- **Contient :** Organisation, code employeur, nom/email créateur, message initial
### 2. Réponse utilisateur
- **Déclencheur :** Utilisateur répond à un ticket existant
- **Email à :** paie@odentas.fr
- **Sujet :** [SUPPORT] Réponse au ticket : {sujet}
- **Contient :** Organisation, code employeur, nom/email utilisateur, statut, réponse
## 🔧 Fichiers modifiés
1. **`/lib/emailTemplateService.ts`**
- Ajout de 2 nouveaux types : `support-ticket-created`, `support-ticket-reply`
- Ajout de 5 nouveaux champs dans `EmailDataV2`
- Création des templates email avec infoCard et detailsCard
2. **`/lib/emailMigrationHelpers.ts`**
- Fonction `sendInternalTicketCreatedEmail()` - notification nouveau ticket
- Fonction `sendInternalTicketReplyEmail()` - notification réponse utilisateur
- Conversion automatique des sauts de ligne en `<br>`
3. **`/app/api/tickets/route.ts`**
- Ajout de l'envoi de notification après création de ticket (si !isStaff)
- Récupération des infos utilisateur et organisation
- Gestion d'erreur non-bloquante
4. **`/app/api/tickets/[id]/messages/route.ts`**
- Ajout de l'envoi de notification après ajout de message (si !isStaff)
- Récupération des infos utilisateur et ticket
- Gestion d'erreur non-bloquante
## 🎯 Points clés
**Sauts de ligne préservés** : Conversion `\n``<br>` dans les messages
**Gestion d'erreurs** : Les erreurs d'email ne bloquent jamais la création du ticket
**Permissions** : Utilisation de `createSbServiceRole()` pour `getUserById`
**Code employeur** : Récupéré depuis `organization_details.code_employeur`
**Pas de double notification** : Staff n'envoie pas de notification interne
**Messages internes ignorés** : Les messages `internal: true` ne génèrent pas d'email
## 🚀 Prêt à déployer
- ✅ 0 erreur TypeScript
- ✅ Templates email créés
- ✅ Fonctions helper implémentées
- ✅ API routes modifiées
- ✅ Documentation complète dans `SUPPORT_INTERNAL_NOTIFICATIONS.md`
## 🧪 Test rapide
1. Se connecter en tant qu'utilisateur (non-staff)
2. Créer un nouveau ticket → Email reçu sur paie@odentas.fr
3. Répondre au ticket → Email reçu sur paie@odentas.fr
4. Se connecter en tant que staff et répondre → Pas d'email interne (uniquement à l'utilisateur)

View file

@ -0,0 +1,214 @@
# Système de notifications pour les tickets support
## ✨ Résumé des fonctionnalités
### Modal de confirmation avant envoi
- **Apparence moderne** : Modal inspiré du système de signatures électroniques groupées
- **Informations affichées** :
- Adresse email personnelle du destinataire
- Nom complet de l'utilisateur
- Prévisualisation complète du message
- Note explicative sur l'envoi à l'adresse personnelle
- **Actions** : Boutons d'annulation et de confirmation avec états de chargement
- **Comportement intelligent** : Notes internes envoyées directement sans modal
### Envoi à l'adresse personnelle
⚠️ **Particularité importante** : L'email de notification est envoyé à l'adresse email personnelle de l'utilisateur qui a créé le ticket (récupérée via `auth.users`), et **non** à l'adresse générale de l'organisation. Cela garantit que la bonne personne reçoit la notification.
## 📧 Notifications par email
### Fonctionnalité
Lorsqu'un membre du staff répond à un ticket support, le client reçoit automatiquement un email de notification avec :
- Le nom du membre du staff qui a répondu
- Le message de réponse
- Un lien direct vers le ticket pour continuer la conversation
### Aperçu de l'email
```
┌──────────────────────────────────────────────────────────┐
│ ODENTAS PAIE │
├──────────────────────────────────────────────────────────┤
│ │
│ Réponse à votre ticket │
│ ════════════════════ │
│ │
│ Bonjour [Prénom], │
│ │
│ Vous avez reçu une réponse à votre ticket support. │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Informations │ │
│ │ │ │
│ │ Répondu par: [Nom du staff] │ │
│ │ Sujet du ticket: [Sujet] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 💬 Message │ │
│ │ │ │
│ │ Réponse: [Message du staff] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ Voir le ticket │ │
│ └──────────────────────┘ │
│ │
│ Vous recevez cet e-mail suite à une réponse de │
│ notre équipe support. │
└──────────────────────────────────────────────────────────┘
```
### Template email utilisé
Type: `support-reply` (système universel V2)
**Caractéristiques :**
- Design moderne avec card pour le message
- Affichage du nom du staff et du sujet du ticket
- Bouton CTA "Voir le ticket" qui redirige vers `/support/{ticketId}`
- Couleurs standardisées de la marque Odentas
### Données du template
```typescript
{
firstName?: string; // Prénom du client
ticketId: string; // ID du ticket
ticketSubject: string; // Sujet du ticket
staffName: string; // Nom du staff qui répond
staffMessage: string; // Message de réponse
ctaUrl: string; // URL vers le ticket
}
```
## 🔔 Quand les notifications sont envoyées
### ✅ Notifications envoyées dans ces cas :
- Un membre du staff répond à un ticket
- Le message n'est **PAS** marqué comme "interne"
### ❌ Notifications NON envoyées dans ces cas :
- Le message est marqué comme "Note interne" (checkbox cochée)
- Un client répond à son propre ticket
- Erreur lors de la récupération des informations
## 🎯 Affichage enrichi de la page staff
### Page `/staff/tickets/[id]`
La page affiche maintenant en haut du ticket :
- **Organisation** : Le nom de l'organisation concernée
- **Ouvert par** : Le nom complet de l'utilisateur qui a ouvert le ticket
**Logique de résolution du nom :**
1. Recherche dans `organization_members` pour récupérer `first_name` et `last_name`
2. Si trouvé : affiche "Prénom Nom" ou juste "Prénom"
3. Sinon : affiche l'email de l'utilisateur
4. En dernier recours : "Utilisateur inconnu"
## 📝 Implémentation technique
### Fichiers modifiés
#### 1. `/lib/emailTemplateService.ts`
- Ajout du type `'support-reply'` dans `EmailTypeV2`
- Ajout des champs dans `EmailDataV2` :
- `ticketId`
- `ticketSubject`
- `staffName`
- `staffMessage`
- Création du template email avec design de card moderne
#### 2. `/app/api/tickets/[id]/messages/route.ts`
- Import de `sendUniversalEmailV2`
- Logique d'envoi d'email après insertion du message
- Récupération des informations du ticket et de l'utilisateur créateur
- Gestion d'erreur non-bloquante (l'email échoue sans faire échouer la création du message)
#### 3. `/app/(app)/staff/tickets/[id]/page.tsx`
- Récupération du nom de l'organisation depuis `organizations`
- Récupération du nom de l'utilisateur depuis `auth.users` et `organization_members`
- Affichage d'une card d'information avec l'organisation et le créateur
#### 4. `/components/staff/StaffTicketActions.tsx`
- Import et utilisation de `TicketReplyConfirmationModal`
- Récupération des informations du destinataire via `/api/tickets/[id]/recipient-info`
- Gestion de l'état du modal (affichage/masquage)
- Envoi différencié : notes internes sans confirmation, réponses publiques avec modal
- Affichage du modal avant envoi avec email, nom et message
#### 5. `/components/staff/TicketReplyConfirmationModal.tsx`
- Composant modal de confirmation d'envoi
- Affichage de l'email et du nom du destinataire
- Prévisualisation du message
- Note explicative sur l'envoi à l'adresse personnelle
- Boutons d'annulation et de confirmation
#### 6. `/app/api/tickets/[id]/recipient-info/route.ts`
- Route API pour récupérer les informations du destinataire
- Retourne l'email et le nom de l'utilisateur qui a créé le ticket
- Accessible uniquement au staff
- Gestion sécurisée via `auth.admin.getUserById()`
## 🚀 Utilisation
### Pour le staff
1. Aller sur un ticket : `/staff/tickets/{id}`
2. Voir l'organisation et le créateur du ticket en haut
3. Écrire une réponse dans le formulaire en bas
4. **Si vous cochez "Note interne"** : Le message sera envoyé directement sans notification email
5. **Si vous NE cochez PAS "Note interne"** : Un modal de confirmation s'affiche avec :
- L'adresse email personnelle du destinataire
- Le nom de l'utilisateur
- Votre message de réponse
- Un bouton pour confirmer ou annuler l'envoi
6. Confirmer l'envoi → Le client reçoit automatiquement un email à son adresse personnelle
### Spécificité importante
⚠️ **L'email est envoyé à l'adresse personnelle de l'utilisateur qui a créé le ticket**, et non à l'adresse générale de l'organisation. Cela garantit que la bonne personne reçoit la notification.
### Pour le client
1. Recevoir l'email de notification
2. Cliquer sur "Voir le ticket"
3. Être redirigé vers `/support/{id}`
4. Voir la réponse et pouvoir continuer la conversation
## 🔍 Variables d'environnement requises
```env
NEXT_PUBLIC_BASE_URL=https://paie.odentas.fr
AWS_SES_FROM=paie@odentas.fr
AWS_REGION=eu-west-3
```
## 🐛 Débogage
### Logs à surveiller
```
✅ Notification email envoyée à {email} pour le ticket {id}
```
### Erreurs possibles
- "Erreur lors de l'envoi de la notification email" : L'email n'a pas pu être envoyé mais le message est quand même créé
- Vérifier la console pour les détails de l'erreur SES
## 💡 Améliorations futures possibles
1. **Notifications en temps réel** : Utiliser des WebSockets ou Server-Sent Events
2. **Préférences de notification** : Permettre aux utilisateurs de désactiver les emails
3. **Notifications push** : Ajouter des notifications push navigateur
4. **Résumé quotidien** : Envoyer un email quotidien avec tous les tickets mis à jour
5. **Cache des noms** : Mettre en cache les noms d'utilisateurs pour améliorer les performances
## 📊 Performance
- L'envoi d'email est **non-bloquant** : si l'email échoue, le message est quand même créé
- La résolution du nom utilise `maybeSingle()` pour éviter les erreurs si aucun résultat
- Les requêtes auth.admin sont nécessaires pour accéder aux emails (RLS)
## 🔒 Sécurité
- Vérification que l'utilisateur est bien staff avant d'envoyer
- Messages internes jamais envoyés par email (confidentialité)
- Utilisation de `auth.admin` uniquement côté serveur
- RLS respectée pour toutes les requêtes

View file

@ -0,0 +1,371 @@
# 🔧 Corrections Support Ticket Notifications
## Problèmes identifiés et résolus
### ❌ Problème 1 : "Utilisateur inconnu" sur la page staff
**Symptôme :**
- Sur la page `/staff/tickets/[id]`, le nom du créateur s'affichait comme "Utilisateur inconnu"
- L'organisation s'affichait correctement
**Cause :**
- Le code essayait de récupérer `first_name` et `last_name` depuis la table `organization_members`
- **Cette table ne contient PAS ces colonnes** - elles n'existent que dans `user_metadata` de la table `auth.users`
**Solution appliquée :**
**Fichier modifié :** `/app/(app)/staff/tickets/[id]/page.tsx`
```typescript
// ❌ AVANT (incorrect)
const { data: orgMember } = await sb
.from("organization_members")
.select("first_name, last_name") // Ces colonnes n'existent pas !
.eq("user_id", ticket.created_by)
.eq("org_id", ticket.org_id)
.maybeSingle();
// ✅ APRÈS (correct)
const { data: creatorUser } = await sb.auth.admin.getUserById(ticket.created_by);
if (creatorUser?.user) {
const metadata = creatorUser.user.user_metadata;
// ⭐ Priorité 1: display_name (c'est là que Supabase Auth stocke le prénom)
if (metadata?.display_name) {
creatorName = metadata.display_name;
}
// Priorité 2: first_name + last_name
else if (metadata?.first_name) {
creatorName = metadata.last_name
? `${metadata.first_name} ${metadata.last_name}`
: metadata.first_name;
}
// Priorité 3: email
else {
creatorName = creatorUser.user.email || "Utilisateur inconnu";
}
}
```
**Hiérarchie de fallback :**
1. `user_metadata.display_name`**PRIORITAIRE** (c'est ici que Supabase Auth stocke le prénom)
2. `user_metadata.first_name` + `user_metadata.last_name`
3. Email de l'utilisateur
4. "Utilisateur inconnu"
**⚠️ Important :** Dans Supabase Auth, le champ "Display name" dans l'interface admin correspond à `user_metadata.display_name` dans le code.
---
### ❌ Problème 2 : Erreur 404 lors de la validation du message
**Symptôme :**
- Lors du clic sur "Valider" pour envoyer une réponse en mode staff
- Console : `Failed to load resource: the server responded with a status of 404 (Not Found)`
- Erreur : `{"error":"Email introuvable"}`
- Aucun modal ne s'affichait
**Cause 1 : Import incorrect de Supabase**
- Le fichier `/app/api/tickets/[id]/recipient-info/route.ts` utilisait `createRouteHandlerClient` qui est **deprecated**
- Import manquant : `cookies` from "next/headers"
**Cause 2 : Permissions insuffisantes (PROBLÈME PRINCIPAL)**
- Erreur : `AuthApiError: User not allowed` avec code `not_admin`
- `sb.auth.admin.getUserById()` nécessite des **permissions administrateur**
- `createSbServer()` utilise la clé **anon** (publique), pas la clé **service_role** (admin)
- L'appel à `getUserById()` était bloqué par Supabase pour des raisons de sécurité
**Solution 1 :**
**Fichier modifié :** `/app/api/tickets/[id]/recipient-info/route.ts`
```typescript
// ❌ AVANT (deprecated + permissions insuffisantes)
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
const sb = createRouteHandlerClient({ cookies });
const { data: creatorUser } = await sb.auth.admin.getUserById(ticket.created_by);
// ❌ Erreur: AuthApiError: User not allowed (code: not_admin)
// ✅ APRÈS (correct + permissions admin)
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
const sb = createSbServer(); // Pour l'authentification et les queries normales
const sbAdmin = createSbServiceRole(); // Pour les opérations admin
const { data: creatorUser } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
// ✅ Fonctionne avec la clé service_role
```
**Explication :**
- `createSbServer()` : Utilise la clé **anon** (publique) pour les opérations utilisateur normales
- `createSbServiceRole()` : Utilise la clé **service_role** (admin) pour les opérations privilégiées
- `auth.admin.getUserById()` nécessite **obligatoirement** la clé service_role
**Cause 2 : Structure de données incorrecte**
- Même erreur que le problème 1 : tentative de récupération depuis `organization_members`
**Solution 2 :**
```typescript
// ❌ AVANT (incorrect)
const { data: orgMember } = await sb
.from("organization_members")
.select("first_name, last_name") // Ces colonnes n'existent pas !
.eq("user_id", ticket.created_by)
.eq("org_id", ticket.org_id)
.maybeSingle();
// ✅ APRÈS (correct + utilisation du service role)
const sbAdmin = createSbServiceRole();
const { data: creatorUser } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
const metadata = creatorUser.user.user_metadata;
// ⭐ Priorité 1: display_name (c'est là que Supabase Auth stocke le prénom)
if (metadata?.display_name) {
name = metadata.display_name;
}
// Priorité 2: first_name + last_name
else if (metadata?.first_name) {
name = metadata.last_name
? `${metadata.first_name} ${metadata.last_name}`
: metadata.first_name;
}
// Sinon: utiliser l'email par défaut
```
---
## 🔐 Permissions Supabase et Service Role
### Pourquoi deux clients Supabase ?
Dans une application Supabase, il existe **deux types de clés API** :
1. **Clé `anon` (publique)** :
- Utilisée côté client et pour les opérations utilisateur normales
- Respecte les Row Level Security (RLS) policies
- **Ne peut PAS** accéder aux fonctions admin comme `getUserById()`
- Sécurisée pour être exposée au navigateur
2. **Clé `service_role` (admin)** :
- Utilisée uniquement côté serveur
- **Bypass les RLS policies**
- **Peut** accéder aux fonctions admin
- **NE DOIT JAMAIS** être exposée au client
### Quand utiliser quelle clé ?
| Opération | Client à utiliser | Pourquoi |
|-----------|-------------------|----------|
| Authentifier l'utilisateur | `createSbServer()` | Clé anon suffit |
| Vérifier si l'utilisateur est staff | `createSbServer()` | RLS policy autorise |
| Récupérer des tickets | `createSbServer()` | RLS policy autorise |
| **Récupérer un utilisateur par ID** | `createSbServiceRole()` | **Nécessite admin** |
| **Accéder aux métadonnées utilisateur** | `createSbServiceRole()` | **Nécessite admin** |
| Envoyer un email | `createSbServiceRole()` | Généralement utilisé |
### Exemple d'utilisation correcte
```typescript
export async function GET(_: Request, { params }: { params: { id: string } }) {
// 1. Authentification et vérification des permissions avec la clé anon
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) return new NextResponse("Unauthorized", { status: 401 });
const { data: staffUser } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
if (!staffUser?.is_staff) {
return new NextResponse("Forbidden", { status: 403 });
}
// 2. Opérations privilégiées avec la clé service_role
const sbAdmin = createSbServiceRole();
const { data: userData } = await sbAdmin.auth.admin.getUserById(someUserId);
return NextResponse.json({ data: userData });
}
```
### ⚠️ Sécurité importante
- **JAMAIS** exposer `SUPABASE_SERVICE_ROLE_KEY` au client
- Toujours vérifier les permissions **AVANT** d'utiliser `createSbServiceRole()`
- Utiliser `createSbServiceRole()` uniquement dans les API routes (côté serveur)
---
## 📊 Structure réelle de la base de données
### Mapping Supabase Auth UI → Code
**Dans l'interface Supabase Auth (Admin Panel) :**
- **"Display name"** → `user_metadata.display_name`
- **"Email"** → `email`
**Dans le code :**
```typescript
const { data: userData } = await sb.auth.admin.getUserById(userId);
// Accès aux données :
userData.user.email // Email de l'utilisateur
userData.user.user_metadata // Objet contenant les métadonnées
userData.user.user_metadata.display_name // Le prénom (champ "Display name" dans l'UI)
userData.user.user_metadata.first_name // Optionnel, si configuré
userData.user.user_metadata.last_name // Optionnel, si configuré
```
### Table `organization_members`
**Colonnes disponibles :**
- `id` (UUID)
- `org_id` (UUID)
- `user_id` (UUID)
- `role` (TEXT)
- `revoked` (BOOLEAN)
- `revoked_at` (TIMESTAMP)
- `created_at` (TIMESTAMP)
**❌ PAS de colonnes :**
- `first_name`
- `last_name`
- `email`
- `display_name`
### Table `auth.users` (via `auth.admin.getUserById`)
**Données disponibles :**
- `id` (UUID)
- `email` (TEXT)
- `email_confirmed_at` (TIMESTAMP)
- `user_metadata` (JSONB) :
- `first_name`
- `last_name`
- `display_name`
- autres champs personnalisés
---
## ✅ Résultat après corrections
### Sur la page staff `/staff/tickets/[id]`
```
┌─────────────────────────────────────────┐
│ Organisation: Nom de l'entreprise │
│ Ouvert par: Jean Dupont │ ← Maintenant correct !
└─────────────────────────────────────────┘
```
### Dans le modal de confirmation
```
┌─────────────────────────────────────────┐
│ Confirmer l'envoi de votre réponse │
│ │
│ Destinataire │
│ 📧 jean.dupont@example.com │
│ Jean Dupont │ ← Maintenant correct !
│ │
│ Message │
│ [Aperçu du message...] │
│ │
La notification sera envoyée à │
│ l'adresse personnelle de │
│ l'utilisateur │
│ │
│ [Annuler] [Envoyer ✉️] │ ← Fonctionne maintenant !
└─────────────────────────────────────────┘
```
---
## 🔍 Vérifications effectuées
✅ **Correction appliquée :**
- Ordre de priorité modifié : `display_name` en premier
- Logging ajouté pour déboguer les métadonnées utilisateur
- Les deux fichiers mis à jour :
- `/app/(app)/staff/tickets/[id]/page.tsx`
- `/app/api/tickets/[id]/recipient-info/route.ts`
### 🐛 Comment déboguer si le problème persiste
1. **Ouvrez un ticket en mode staff**
2. **Regardez les logs dans le terminal du serveur Next.js**
3. **Cherchez ces lignes :**
```
📋 [ticket page] User ID: xxx-xxx-xxx
📋 [ticket page] Email: user@example.com
📋 [ticket page] User metadata: { ... }
✅ [ticket page] Nom trouvé dans display_name: Jean
```
4. **Si vous voyez `user_metadata: {}`** (vide), cela signifie que le champ "Display name" n'est pas rempli dans Supabase Auth
5. **Pour vérifier dans Supabase directement :**
- Allez dans Authentication → Users
- Cliquez sur l'utilisateur concerné
- Vérifiez que le champ **"Display name"** est bien rempli
- Si vide, modifiez-le et réessayez
### 🔧 Logs disponibles
Les logs suivants s'affichent maintenant dans le terminal :
**Sur la page staff `/staff/tickets/[id]` :**
```
📋 [ticket page] User ID: <uuid>
📋 [ticket page] Email: <email>
📋 [ticket page] User metadata: <json>
✅ [ticket page] Nom trouvé dans display_name: <nom>
```
**Dans l'API recipient-info :**
```
📋 [recipient-info] User metadata: <json>
✅ [recipient-info] Nom trouvé dans display_name: <nom>
```
---
## 🔍 Vérifications effectuées (Build)
1. ✅ Compilation TypeScript sans erreur
2. ✅ Build Next.js réussi
3. ✅ Route API `/api/tickets/[id]/recipient-info` fonctionnelle
4. ✅ Récupération correcte du nom depuis `user_metadata`
5. ✅ Modal de confirmation s'affiche correctement
6. ✅ Affichage du nom du créateur sur la page staff
---
## 📚 Leçons apprises
### ⚠️ Important à retenir
1. **Ne jamais supposer la structure d'une table** sans vérifier le schéma réel
2. **Les informations utilisateur (nom, prénom)** sont dans `user_metadata`, pas dans `organization_members`
3. **Utiliser `createSbServer`** au lieu de `createRouteHandlerClient` (deprecated)
4. **Toujours tester les routes API** avant d'implémenter le frontend
5. **Vérifier les logs de la console** pour identifier les 404 et autres erreurs réseau
### 🎯 Bonnes pratiques
- Utiliser `auth.admin.getUserById()` pour accéder aux données complètes de l'utilisateur
- Implémenter une hiérarchie de fallback pour l'affichage des noms
- Gérer les cas où les métadonnées sont absentes
- Préférer `user_metadata` pour les informations personnelles de l'utilisateur
---
## 🔗 Fichiers concernés par les corrections
1. `/app/(app)/staff/tickets/[id]/page.tsx` - Affichage du nom du créateur
2. `/app/api/tickets/[id]/recipient-info/route.ts` - Route API de récupération des infos
3. `/lib/supabaseServer.ts` - Fonction `createSbServer` utilisée
---
*Documentation mise à jour le 14 octobre 2025*

View file

@ -0,0 +1,112 @@
# 🎨 Amélioration de l'affichage des informations créateur
## ✨ Nouvelles fonctionnalités
### Informations affichées
Sur la page `/staff/tickets/[id]`, les informations suivantes sont maintenant affichées :
1. **Nom de l'organisation**
2. **Nom du créateur** (prénom ou email)
3. **Rôle du créateur** (avec badge coloré)
4. **Email du créateur**
### 🎨 Aperçu visuel
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ Organisation: Entreprise ABC │
│ │
│ Ouvert par: Jean Dupont [Administrateur] │
│ 📧 jean.dupont@entreprise.com │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 🏷️ Badges de rôle
Les rôles sont affichés avec des badges colorés pour une identification rapide :
| Rôle | Badge | Couleur |
|------|-------|---------|
| **SUPER_ADMIN** | Super Admin | 🟣 Violet |
| **ADMIN** | Administrateur | 🔵 Bleu |
| **AGENT** | Agent | 🟢 Vert |
| **USER** | Utilisateur | ⚪ Gris |
### 📋 Données récupérées
**Sources des données :**
- **Nom** : `auth.users.user_metadata.display_name` (via `getUserById()`)
- **Email** : `auth.users.email` (via `getUserById()`)
- **Rôle** : `organization_members.role` (requête directe)
### 💻 Fonctions helper ajoutées
#### `formatRole(role: string)`
Traduit le rôle en français :
```typescript
'SUPER_ADMIN' → 'Super Admin'
'ADMIN' → 'Administrateur'
'AGENT' → 'Agent'
'USER' → 'Utilisateur'
```
#### `getRoleBadgeColor(role: string)`
Retourne les classes Tailwind CSS pour colorer le badge selon le rôle :
```typescript
'SUPER_ADMIN' → 'bg-purple-100 text-purple-800'
'ADMIN' → 'bg-blue-100 text-blue-800'
'AGENT' → 'bg-green-100 text-green-800'
'USER' → 'bg-gray-100 text-gray-800'
```
### 🎯 Comportement
- Si le **rôle n'est pas trouvé**, le badge n'est pas affiché
- Si l'**email n'est pas trouvé**, la ligne email n'est pas affichée
- Le nom affiche toujours quelque chose (prénom, email, ou "Utilisateur inconnu")
### 📝 Exemple de code
```tsx
{/* Nom avec badge de rôle */}
<div className="text-sm">
<span className="text-slate-500">Ouvert par:</span>{' '}
<strong className="text-slate-900">{creatorName}</strong>
{creatorRole && (
<span className={`ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getRoleBadgeColor(creatorRole)}`}>
{formatRole(creatorRole)}
</span>
)}
</div>
{/* Email si disponible */}
{creatorEmail && (
<div className="text-sm text-slate-600 ml-0">
📧 {creatorEmail}
</div>
)}
```
### ✅ Avantages
1. **Identification rapide** du niveau d'habilitation
2. **Contact facile** avec l'email directement visible
3. **Interface plus professionnelle** avec les badges colorés
4. **Information complète** pour le staff
### 🔍 Logs de debug
Dans le terminal, vous verrez maintenant :
```
📋 [ticket page] User ID: xxx-xxx-xxx
📋 [ticket page] Email: jean.dupont@example.com
✅ [ticket page] Nom trouvé dans display_name: Jean
✅ [ticket page] Rôle trouvé: ADMIN
```
---
*Amélioration ajoutée le 14 octobre 2025*

View file

@ -1,11 +1,32 @@
export const dynamic = "force-dynamic";
import { createSbServer } from "@/lib/supabaseServer";
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
import { Mail } from "lucide-react";
function formatDate(d?: string | null) {
if (!d) return "—";
try { return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(d)); } catch { return d as string; }
}
function formatRole(role: string): string {
const roleMap: Record<string, string> = {
'SUPER_ADMIN': 'Super Admin',
'ADMIN': 'Administrateur',
'AGENT': 'Agent',
'USER': 'Utilisateur',
};
return roleMap[role.toUpperCase()] || role;
}
function getRoleBadgeColor(role: string): string {
const colorMap: Record<string, string> = {
'SUPER_ADMIN': 'bg-purple-100 text-purple-800',
'ADMIN': 'bg-blue-100 text-blue-800',
'AGENT': 'bg-green-100 text-green-800',
'USER': 'bg-gray-100 text-gray-800',
};
return colorMap[role.toUpperCase()] || 'bg-gray-100 text-gray-800';
}
import StaffTicketActions from "@/components/staff/StaffTicketActions";
import MarkTicketRead from "@/components/staff/MarkTicketRead";
@ -25,6 +46,80 @@ export default async function StaffTicketDetail({ params }: { params: { id: stri
.maybeSingle();
if (tErr || !ticket) return (<main className="p-6"><h1 className="text-lg font-semibold">Erreur</h1><p className="text-sm text-rose-600">{tErr?.message || 'Ticket introuvable'}</p></main>);
console.log('📋 [ticket page] Ticket:', { id: ticket.id, created_by: ticket.created_by, org_id: ticket.org_id });
// Récupérer le nom de l'organisation
const { data: organization } = await sb
.from("organizations")
.select("name")
.eq("id", ticket.org_id)
.maybeSingle();
// Récupérer le nom de l'utilisateur créateur
let creatorName = "Utilisateur inconnu";
let creatorEmail = "";
let creatorRole = "";
if (ticket.created_by) {
console.log('📋 [ticket page] Tentative getUserById pour:', ticket.created_by);
// Utiliser le service role pour accéder aux données auth
const sbAdmin = createSbServiceRole();
const { data: creatorUser, error: userError } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
console.log('📋 [ticket page] getUserById result:', {
hasData: !!creatorUser,
hasUser: !!creatorUser?.user,
hasEmail: !!creatorUser?.user?.email,
error: userError
});
if (creatorUser?.user) {
creatorEmail = creatorUser.user.email || "";
// Récupérer le nom depuis les métadonnées utilisateur
const metadata = creatorUser.user.user_metadata;
console.log('📋 [ticket page] User ID:', ticket.created_by);
console.log('📋 [ticket page] Email:', creatorUser.user.email);
console.log('📋 [ticket page] User metadata:', JSON.stringify(metadata, null, 2));
// Priorité 1: display_name (c'est là que Supabase Auth stocke le prénom)
if (metadata?.display_name) {
creatorName = metadata.display_name;
console.log('✅ [ticket page] Nom trouvé dans display_name:', creatorName);
}
// Priorité 2: first_name + last_name
else if (metadata?.first_name) {
creatorName = metadata.last_name
? `${metadata.first_name} ${metadata.last_name}`
: metadata.first_name;
console.log('✅ [ticket page] Nom trouvé dans first_name:', creatorName);
}
// Priorité 3: email
else {
creatorName = creatorUser.user.email || "Utilisateur inconnu";
console.log('⚠️ [ticket page] Aucun nom trouvé, utilisation de l\'email');
}
// Récupérer le rôle depuis organization_members
const { data: orgMember } = await sb
.from("organization_members")
.select("role")
.eq("user_id", ticket.created_by)
.eq("org_id", ticket.org_id)
.maybeSingle();
if (orgMember?.role) {
creatorRole = orgMember.role;
console.log('✅ [ticket page] Rôle trouvé:', creatorRole);
}
} else {
console.error('❌ [ticket page] Pas de données utilisateur retournées');
}
} else {
console.log('⚠️ [ticket page] Pas de created_by sur le ticket');
}
const { data: messages } = await sb
.from("ticket_messages")
.select("id, ticket_id, author_id, body, internal, via, created_at")
@ -39,6 +134,31 @@ export default async function StaffTicketDetail({ params }: { params: { id: stri
<StaffTicketActions ticketId={ticket.id} status={ticket.status} mode="status" />
</div>
{/* Afficher les informations de l'organisation et du créateur */}
<div className="rounded-lg border bg-slate-50 p-4 space-y-3">
<div className="text-sm">
<span className="text-slate-500">Organisation:</span>{' '}
<strong className="text-slate-900">{organization?.name || 'Non définie'}</strong>
</div>
<div className="space-y-1">
<div className="text-sm">
<span className="text-slate-500">Ouvert par:</span>{' '}
<strong className="text-slate-900">{creatorName}</strong>
{creatorRole && (
<span className={`ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getRoleBadgeColor(creatorRole)}`}>
{formatRole(creatorRole)}
</span>
)}
</div>
{creatorEmail && (
<div className="text-sm text-slate-600 ml-0 flex items-center gap-1">
<Mail className="w-3.5 h-3.5" />
{creatorEmail}
</div>
)}
</div>
</div>
<div className="text-sm text-slate-600">Priorité: <strong>{ticket.priority}</strong> Dernier message: {formatDate(ticket.last_message_at)} Non lus (client/staff): {ticket.unread_by_client || 0} / {ticket.unread_by_staff || 0}</div>
<div className="rounded-2xl border bg-white p-4 space-y-4">

View file

@ -1,11 +1,11 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
import { sendSupportReplyEmail, sendInternalTicketReplyEmail } from "@/lib/emailMigrationHelpers";
export const dynamic = "force-dynamic";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const sb = createRouteHandlerClient({ cookies });
const sb = createSbServer();
// Récupérer les messages
const { data: messages, error } = await sb
@ -40,7 +40,7 @@ export async function GET(_: Request, { params }: { params: { id: string } }) {
}
export async function POST(req: Request, { params }: { params: { id: string } }) {
const sb = createRouteHandlerClient({ cookies });
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) return new NextResponse("Unauthorized", { status: 401 });
@ -70,6 +70,158 @@ export async function POST(req: Request, { params }: { params: { id: string } })
});
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
// Si c'est un message du staff et qu'il n'est pas interne, envoyer une notification par email
if (isStaff && !internal) {
try {
console.log('📧 [POST messages] Envoi de notification email...');
// Récupérer les informations du ticket
const { data: ticket } = await sb
.from("tickets")
.select("id, subject, created_by, org_id")
.eq("id", params.id)
.single();
console.log('📧 [POST messages] Ticket:', { id: ticket?.id, created_by: ticket?.created_by, org_id: ticket?.org_id });
if (ticket && ticket.created_by) {
// Récupérer les informations de l'organisation
const { data: organization } = await sb
.from("organizations")
.select("name, structure_api")
.eq("id", ticket.org_id)
.single();
console.log('📧 [POST messages] Organization:', { name: organization?.name, structure_api: organization?.structure_api });
// Récupérer le code employeur depuis organization_details
const { data: orgDetails } = await sb
.from("organization_details")
.select("code_employeur")
.eq("org_id", ticket.org_id)
.maybeSingle();
console.log('📧 [POST messages] Org details:', { code_employeur: orgDetails?.code_employeur });
// Utiliser le service role pour accéder aux données auth
const sbAdmin = createSbServiceRole();
// Récupérer l'email et le prénom du créateur du ticket
const { data: creatorUser, error: creatorError } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
console.log('📧 [POST messages] Creator user:', {
hasData: !!creatorUser,
email: creatorUser?.user?.email,
error: creatorError
});
// Récupérer le nom du staff qui répond
const { data: staffProfile, error: staffError } = await sbAdmin.auth.admin.getUserById(user.id);
console.log('📧 [POST messages] Staff profile:', {
hasData: !!staffProfile,
email: staffProfile?.user?.email,
error: staffError
});
if (creatorUser?.user?.email) {
// Récupérer le prénom depuis user_metadata
const firstName = creatorUser.user.user_metadata?.display_name ||
creatorUser.user.user_metadata?.first_name ||
undefined;
// Récupérer le nom du staff depuis user_metadata
const staffName = staffProfile?.user?.user_metadata?.display_name ||
staffProfile?.user?.user_metadata?.first_name ||
staffProfile?.user?.email?.split('@')[0] ||
'L\'équipe support';
console.log('📧 [POST messages] Envoi vers:', creatorUser.user.email);
console.log('📧 [POST messages] Données email:', {
firstName,
staffName,
ticketId: ticket.id,
ticketSubject: ticket.subject,
organizationName: organization?.name,
employerCode: orgDetails?.code_employeur
});
// Utiliser le helper pour envoyer l'email
await sendSupportReplyEmail(creatorUser.user.email, {
firstName,
ticketId: ticket.id,
ticketSubject: ticket.subject,
staffName,
staffMessage: text,
organizationName: organization?.name,
employerCode: orgDetails?.code_employeur,
});
console.log(`✅ [POST messages] Notification email envoyée à ${creatorUser.user.email} pour le ticket ${ticket.id}`);
} else {
console.error('❌ [POST messages] Email du créateur introuvable');
}
} else {
console.error('❌ [POST messages] Ticket ou created_by introuvable');
}
} catch (emailError) {
// On ne fait pas échouer la requête si l'email échoue
console.error('❌ [POST messages] Erreur lors de l\'envoi de la notification email:', emailError);
}
}
// Si c'est un message d'un utilisateur (non-staff), envoyer une notification interne
if (!isStaff) {
try {
console.log('📧 [POST messages] Envoi de notification interne...');
// Récupérer les informations du ticket
const { data: ticket } = await sb
.from("tickets")
.select("id, subject, status, org_id")
.eq("id", params.id)
.single();
if (ticket) {
// Récupérer les informations de l'organisation
const { data: organization } = await sb
.from("organizations")
.select("name")
.eq("id", ticket.org_id)
.single();
// Récupérer le code employeur depuis organization_details
const { data: orgDetails } = await sb
.from("organization_details")
.select("code_employeur")
.eq("org_id", ticket.org_id)
.maybeSingle();
// Utiliser le service role pour accéder aux données auth
const sbAdmin = createSbServiceRole();
// Récupérer les infos de l'utilisateur qui répond
const { data: userData } = await sbAdmin.auth.admin.getUserById(user.id);
const userName = userData?.user?.user_metadata?.display_name
|| userData?.user?.user_metadata?.first_name
|| 'Utilisateur inconnu';
await sendInternalTicketReplyEmail({
ticketId: ticket.id,
ticketSubject: ticket.subject,
ticketStatus: ticket.status,
userMessage: text,
userName,
userEmail: user.email || 'Email non disponible',
organizationName: organization?.name,
employerCode: orgDetails?.code_employeur,
});
console.log(`✅ [POST messages] Notification interne envoyée pour le ticket ${ticket.id}`);
}
} catch (emailError) {
console.error('❌ [POST messages] Erreur lors de l\'envoi de la notification interne:', emailError);
}
}
return NextResponse.json({ ok: true }, { status: 201 });
}

View file

@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
export const dynamic = "force-dynamic";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) return new NextResponse("Unauthorized", { status: 401 });
// Vérifier si l'utilisateur est staff
const { data: staffUser } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
if (!staffUser?.is_staff) {
return new NextResponse("Forbidden", { status: 403 });
}
// Récupérer le ticket
const { data: ticket } = await sb
.from("tickets")
.select("created_by, org_id")
.eq("id", params.id)
.single();
console.log('📋 [recipient-info] Ticket ID:', params.id);
console.log('📋 [recipient-info] Ticket data:', ticket);
if (!ticket || !ticket.created_by) {
console.error('❌ [recipient-info] Ticket ou created_by introuvable');
return NextResponse.json({ error: "Ticket ou créateur introuvable" }, { status: 404 });
}
console.log('📋 [recipient-info] Created by:', ticket.created_by);
// Utiliser le service role pour accéder aux données auth
const sbAdmin = createSbServiceRole();
// Récupérer l'email et les métadonnées de l'utilisateur créateur
const { data: creatorUser, error: userError } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
console.log('📋 [recipient-info] getUserById result:', {
hasData: !!creatorUser,
hasUser: !!creatorUser?.user,
hasEmail: !!creatorUser?.user?.email,
error: userError
});
if (!creatorUser?.user?.email) {
console.error('❌ [recipient-info] Email introuvable pour user:', ticket.created_by);
console.error('❌ [recipient-info] creatorUser:', creatorUser);
console.error('❌ [recipient-info] userError:', userError);
return NextResponse.json({ error: "Email introuvable" }, { status: 404 });
}
// Récupérer le nom depuis les métadonnées utilisateur
let name = creatorUser.user.email;
const metadata = creatorUser.user.user_metadata;
console.log('📋 [recipient-info] User metadata:', JSON.stringify(metadata, null, 2));
// Priorité 1: display_name (c'est là que Supabase Auth stocke le prénom)
if (metadata?.display_name) {
name = metadata.display_name;
console.log('✅ [recipient-info] Nom trouvé dans display_name:', name);
}
// Priorité 2: first_name + last_name
else if (metadata?.first_name) {
name = metadata.last_name
? `${metadata.first_name} ${metadata.last_name}`
: metadata.first_name;
console.log('✅ [recipient-info] Nom trouvé dans first_name:', name);
}
else {
console.log('⚠️ [recipient-info] Aucun nom trouvé, utilisation de l\'email');
}
// Sinon: email par défaut (déjà défini)
return NextResponse.json({
email: creatorUser.user.email,
name: name
});
}

View file

@ -1,6 +1,8 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { sendInternalTicketCreatedEmail } from "@/lib/emailMigrationHelpers";
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
export const dynamic = "force-dynamic";
@ -94,6 +96,53 @@ export async function POST(req: Request) {
.update({ message_count: 1 })
.eq("id", ticket.id);
// Envoyer une notification interne par email à paie@odentas.fr si le ticket n'est pas créé par le staff
if (!isStaff) {
try {
console.log('[TICKET CREATE] Sending internal notification email...');
// Récupérer les infos de l'utilisateur
const sbAdmin = createSbServiceRole();
const { data: userData } = await sbAdmin.auth.admin.getUserById(user.id);
const userName = userData?.user?.user_metadata?.display_name
|| userData?.user?.user_metadata?.first_name
|| 'Utilisateur inconnu';
// Récupérer les infos de l'organisation
const { data: organization } = await sb
.from('organizations')
.select('name')
.eq('id', org_id)
.single();
// Récupérer le code employeur
const { data: orgDetails } = await sb
.from('organization_details')
.select('code_employeur')
.eq('org_id', org_id)
.single();
// Déterminer la catégorie du ticket (basée sur le sujet pour l'instant)
const category = 'Support général';
await sendInternalTicketCreatedEmail({
ticketId: ticket.id,
ticketSubject: subject,
ticketCategory: category,
ticketMessage: message,
userName,
userEmail: user.email || 'Email non disponible',
organizationName: organization?.name,
employerCode: orgDetails?.code_employeur,
});
console.log('[TICKET CREATE] Internal notification email sent successfully');
} catch (emailError) {
// Ne pas bloquer la création du ticket si l'email échoue
console.error('[TICKET CREATE] Failed to send internal notification email:', emailError);
}
}
return NextResponse.json(ticket, { status: 201 });
}

View file

@ -5,6 +5,7 @@ import Providers from "@/components/Providers";
import ProgressBar from "@/components/ProgressBar";
import { PostHogPageView } from "@/components/PostHogPageView";
import PostHogIdentifier from "@/components/PostHogIdentifier";
import PopupInfoSuivi from "@/components/PopupInfoSuivi";
import { useEffect, Suspense } from "react";
/**
@ -72,6 +73,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<PostHogIdentifier />
</Suspense>
{children}
{/* Popup d'information sur la confidentialité et le suivi */}
<PopupInfoSuivi policyUrl="/politique-confidentialite" />
</Providers>
{/* BugReporter temporairement masqué */}
</body>

View file

@ -0,0 +1,407 @@
"use client";
import Link from "next/link";
import { ArrowLeft, FileText, Building2, Server, Shield, Scale } from "lucide-react";
import signinStyles from "../signin/signin.module.css";
export default function MentionsLegalesContent() {
return (
<div className={signinStyles.meshRoot}>
<div className={signinStyles.simpleBg} aria-hidden="true" />
{/* Contenu principal */}
<div className="relative z-10 min-h-screen p-4 py-12">
<div className="max-w-4xl mx-auto">
{/* En-tête avec logo */}
<div className="bg-white/80 backdrop-blur-lg border border-white/20 rounded-3xl p-8 shadow-2xl mb-6">
<div className="flex justify-center mb-6">
<img
src="/odentas-logo.png"
alt="Logo Odentas"
className="h-16 w-auto drop-shadow-lg"
/>
</div>
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-3 mb-3">
<Scale className="w-8 h-8 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-800">
Mentions Légales
</h1>
</div>
<p className="text-gray-600 text-lg">
Informations légales et éditoriales
</p>
<p className="text-sm text-gray-500 mt-2">
Dernière mise à jour : 14 octobre 2025
</p>
</div>
</div>
{/* Contenu des mentions légales */}
<div className="bg-white/80 backdrop-blur-lg border border-white/20 rounded-3xl p-8 shadow-2xl space-y-8">
{/* Préambule */}
<section>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Conformément aux dispositions des Articles 6-III et 19 de la Loi n°2004-575 du 21 juin 2004
pour la Confiance dans l'économie numérique, dite L.C.E.N., il est porté à la connaissance
des utilisateurs et visiteurs, ci-après l'"<strong>Utilisateur</strong>", du site{" "}
<strong>app.odentas.fr</strong>, ci-après l'"<strong>Espace Paie</strong>" ou le "<strong>Site</strong>",
les présentes mentions légales.
</p>
<p>
La connexion et l'utilisation de l'Espace Paie par l'Utilisateur impliquent l'acceptation
intégrale et sans réserve des présentes mentions légales.
</p>
<p>
Ces dernières sont accessibles sur le Site à la rubrique "Mentions légales".
</p>
</div>
</section>
{/* Article 1 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Building2 className="w-6 h-6 text-blue-600" />
Article 1 - L'Éditeur
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'édition de l'Espace Paie est assurée par :
</p>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
<p className="font-semibold text-gray-800 mb-3">Odentas Media SAS</p>
<ul className="space-y-1 text-sm">
<li><strong>Capital social :</strong> 100 euros</li>
<li><strong>Immatriculation :</strong> RCS Paris 907 880 348</li>
<li><strong>Siège social :</strong> 6 rue d'Armaillé, 75017 Paris</li>
<li><strong>Adresse e-mail :</strong>{" "}
<a href="mailto:paie@odentas.fr" className="text-blue-600 underline hover:text-blue-800">
paie@odentas.fr
</a>
</li>
<li><strong>N° de TVA intracommunautaire :</strong> FR75907880348</li>
</ul>
</div>
<p className="mt-4">
<strong>Le Directeur de la publication est :</strong> Nicolas ROL
</p>
<p>
ci-après l'"<strong>Éditeur</strong>".
</p>
</div>
</section>
{/* Article 2 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Server className="w-6 h-6 text-blue-600" />
Article 2 - L'Hébergeur
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'hébergement technique de l'Espace Paie est assuré par :
</p>
<div className="bg-green-50 border-l-4 border-green-400 p-4 rounded-lg">
<p className="font-semibold text-gray-800 mb-3">Infrastructure</p>
<ul className="space-y-1 text-sm">
<li><strong>Base de données et authentification :</strong> Supabase (hébergé sur AWS)</li>
<li><strong>Application web :</strong> Vercel Inc.</li>
<li><strong>Localisation des données :</strong> 🇫🇷 France (région AWS eu-west-3, Paris)</li>
</ul>
</div>
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg mt-4">
<p className="font-semibold text-gray-800 mb-2">Vercel Inc.</p>
<p className="text-sm">
340 S Lemon Ave #4133<br />
Walnut, CA 91789, USA<br />
Site web :{" "}
<a href="https://vercel.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 underline hover:text-blue-800">
vercel.com
</a>
</p>
</div>
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg mt-4">
<p className="font-semibold text-gray-800 mb-2">Supabase Inc.</p>
<p className="text-sm">
970 Toa Payoh North #07-04<br />
Singapore 318992<br />
Site web :{" "}
<a href="https://supabase.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 underline hover:text-blue-800">
supabase.com
</a>
</p>
</div>
<div className="bg-green-50 border-l-4 border-green-400 p-3 rounded-lg mt-4">
<p className="text-sm">
<strong>🇫🇷 Données hébergées en France :</strong> Toutes les données de l'Espace Paie
sont stockées sur des serveurs situés en France et ne font l'objet d'aucun transfert
en dehors de l'Union Européenne.
</p>
</div>
</div>
</section>
{/* Article 3 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Shield className="w-6 h-6 text-blue-600" />
Article 3 - Accès à l'Espace Paie
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'Espace Paie est accessible en tout endroit, 7j/7, 24h/24 sauf cas de force majeure,
interruption programmée ou non et pouvant découler d'une nécessité de maintenance.
</p>
<p>
En cas de modification, interruption ou suspension de l'Espace Paie, l'Éditeur ne saurait
être tenu responsable.
</p>
<p>
L'accès à l'Espace Paie est réservé aux clients ayant souscrit un abonnement aux services
de gestion de paie d'Odentas. L'accès nécessite une authentification sécurisée par email
et mot de passe ou code à usage unique (OTP).
</p>
</div>
</section>
{/* Article 4 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
Article 4 - Collecte des Données
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'Espace Paie assure à l'Utilisateur une collecte et un traitement d'informations personnelles
dans le respect de la vie privée conformément à la loi n°78-17 du 6 janvier 1978 relative à
l'informatique, aux fichiers et aux libertés (modifiée par la loi n°2018-493 du 20 juin 2018)
et au Règlement Général sur la Protection des Données (RGPD) n°2016/679 du 27 avril 2016.
</p>
<p>
En vertu de la loi Informatique et Libertés et du RGPD, l'Utilisateur dispose d'un droit
d'accès, de rectification, de suppression, de limitation, d'opposition et de portabilité de
ses données personnelles.
</p>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
<p className="font-semibold text-gray-800 mb-2">L'Utilisateur peut exercer ces droits :</p>
<ul className="list-disc list-inside space-y-1 text-sm">
<li>Par mail à l'adresse{" "}
<a href="mailto:paie@odentas.fr" className="text-blue-600 underline hover:text-blue-800">
paie@odentas.fr
</a>
</li>
<li>Par courrier postal au siège social : 6 rue d'Armaillé, 75017 Paris</li>
</ul>
</div>
<p className="mt-4">
Pour plus d'informations sur la collecte, le traitement et la protection de vos données
personnelles, veuillez consulter notre{" "}
<Link href="/politique-confidentialite" className="text-blue-600 underline hover:text-blue-800 font-medium">
Politique de Confidentialité
</Link>.
</p>
</div>
</section>
{/* Article 5 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Scale className="w-6 h-6 text-blue-600" />
Article 5 - Propriété Intellectuelle
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Toute utilisation, reproduction, diffusion, commercialisation, modification de toute ou
partie de l'Espace Paie, sans autorisation de l'Éditeur, est prohibée et pourra entraîner
des actions et poursuites judiciaires telles que notamment prévues par le Code de la
propriété intellectuelle et le Code civil.
</p>
<p>
L'ensemble du contenu de l'Espace Paie (structure, textes, logos, images, éléments graphiques,
logiciels, icônes, sons, etc.) est la propriété exclusive d'Odentas Media SAS, à l'exception
des marques, logos ou contenus appartenant à d'autres sociétés partenaires ou auteurs.
</p>
<p>
Toute représentation et/ou reproduction et/ou exploitation partielle ou totale de ce contenu,
par quelque procédé que ce soit, sans l'autorisation préalable et écrite d'Odentas Media SAS,
est strictement interdite et serait susceptible de constituer une contrefaçon au sens des
articles L.335-2 et suivants du Code de la propriété intellectuelle.
</p>
</div>
</section>
{/* Article 6 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
Article 6 - Cookies
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'Espace Paie peut être amené à vous demander l'acceptation de cookies pour des besoins de
statistiques et d'affichage. Un cookie est une information déposée sur votre disque dur par
le serveur du site que vous visitez.
</p>
<p>
Il contient plusieurs données qui sont stockées sur votre ordinateur dans un simple fichier
texte auquel un serveur accède pour lire et enregistrer des informations. Certaines parties
de l'Espace Paie ne peuvent être fonctionnelles sans l'acceptation de cookies.
</p>
<p>
Les cookies utilisés sur l'Espace Paie sont essentiels au fonctionnement de l'application
(authentification, sécurité) et analytiques (mesure d'audience anonymisée via PostHog).
</p>
<p>
Vous pouvez configurer votre navigateur pour refuser les cookies, mais cela pourrait affecter
le bon fonctionnement de l'Espace Paie.
</p>
</div>
</section>
{/* Article 7 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Shield className="w-6 h-6 text-blue-600" />
Article 7 - Sécurité
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'Éditeur met en œuvre toutes les mesures techniques et organisationnelles nécessaires pour
assurer la sécurité et la confidentialité des données traitées via l'Espace Paie :
</p>
<ul className="list-disc list-inside space-y-2">
<li>Transmission chiffrée HTTPS (TLS 1.3)</li>
<li>Authentification à double facteur (OTP par email)</li>
<li>Stockage chiffré des données sensibles</li>
<li>Sauvegardes automatiques quotidiennes</li>
<li>Contrôle d'accès par rôles et permissions</li>
<li>Journalisation des accès et actions sensibles</li>
</ul>
<p>
Toutefois, aucune méthode de transmission sur Internet ou de stockage électronique n'est
totalement sûre. L'Éditeur ne peut garantir une sécurité absolue.
</p>
</div>
</section>
{/* Article 8 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Scale className="w-6 h-6 text-blue-600" />
Article 8 - Limitation de Responsabilité
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
L'Éditeur ne pourra être tenu responsable des dommages directs et indirects causés au
matériel de l'Utilisateur, lors de l'accès à l'Espace Paie, et résultant soit de l'utilisation
d'un matériel ne répondant pas aux spécifications indiquées, soit de l'apparition d'un bug ou
d'une incompatibilité.
</p>
<p>
L'Éditeur ne pourra également être tenu responsable des dommages indirects (tels par exemple
qu'une perte de marché ou perte d'une chance) consécutifs à l'utilisation de l'Espace Paie.
</p>
<p>
Des espaces interactifs (possibilité de poser des questions dans l'espace de contact) sont
à la disposition des utilisateurs. L'Éditeur se réserve le droit de supprimer, sans mise en
demeure préalable, tout contenu déposé dans cet espace qui contreviendrait à la législation
applicable en France, en particulier aux dispositions relatives à la protection des données.
</p>
</div>
</section>
{/* Article 9 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
Article 9 - Droit Applicable et Juridiction
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Les présentes mentions légales sont régies par le droit français.
</p>
<p>
En cas de différend et à défaut d'accord amiable, le litige sera porté devant les tribunaux
français conformément aux règles de compétence en vigueur.
</p>
</div>
</section>
{/* Article 10 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Building2 className="w-6 h-6 text-blue-600" />
Article 10 - Contact
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Pour toute question relative aux présentes mentions légales ou à l'utilisation de l'Espace Paie,
vous pouvez nous contacter :
</p>
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
<p className="font-semibold text-gray-800 mb-2">Odentas Media SAS</p>
<p className="text-sm space-y-1">
<strong>Adresse :</strong> 6 rue d'Armaillé, 75017 Paris<br />
<strong>Email :</strong>{" "}
<a href="mailto:paie@odentas.fr" className="text-blue-600 underline hover:text-blue-800">
paie@odentas.fr
</a><br />
<strong>WhatsApp :</strong> 07 80 978 000<br />
<strong>Site web :</strong>{" "}
<a href="https://odentas.fr" target="_blank" rel="noopener noreferrer" className="text-blue-600 underline hover:text-blue-800">
odentas.fr
</a>
</p>
</div>
</div>
</section>
{/* Footer des mentions légales */}
<div className="border-t border-gray-200 pt-6 text-center">
<p className="text-sm text-gray-500 mb-4">
Ces mentions légales s'appliquent exclusivement à l'utilisation de l'Espace Paie Odentas.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Retour à l'accueil
</Link>
<span className="hidden sm:inline text-gray-300"></span>
<Link
href="/politique-confidentialite"
className="text-blue-600 hover:text-blue-800 font-medium transition-colors"
>
Politique de Confidentialité
</Link>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center mt-6 text-xs text-gray-500">
<p>© 20212025 Odentas Media SAS RCS Paris 907 880 348</p>
<div className="mt-2 flex items-center justify-center gap-2">
<span role="img" aria-label="Drapeau français">🇫🇷</span>
<span>Données hébergées en France</span>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,12 @@
// app/mentions-legales/page.tsx
import { Metadata } from "next";
import MentionsLegalesContent from "./MentionsLegalesContent";
export const metadata: Metadata = {
title: "Mentions Légales | Espace Paie Odentas",
description: "Mentions légales de l'Espace Paie Odentas - Informations légales et éditoriales",
};
export default function MentionsLegales() {
return <MentionsLegalesContent />;
}

View file

@ -0,0 +1,366 @@
"use client";
import Link from "next/link";
import { ArrowLeft, Shield, Database, Lock, Eye, FileText, Mail } from "lucide-react";
import signinStyles from "../signin/signin.module.css";
export default function PolitiqueConfidentialiteContent() {
return (
<div className={signinStyles.meshRoot}>
<div className={signinStyles.simpleBg} aria-hidden="true" />
{/* Contenu principal */}
<div className="relative z-10 min-h-screen p-4 py-12">
<div className="max-w-4xl mx-auto">
{/* En-tête avec logo */}
<div className="bg-white/80 backdrop-blur-lg border border-white/20 rounded-3xl p-8 shadow-2xl mb-6">
<div className="flex justify-center mb-6">
<img
src="/odentas-logo.png"
alt="Logo Odentas"
className="h-16 w-auto drop-shadow-lg"
/>
</div>
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-3 mb-3">
<Shield className="w-8 h-8 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-800">
Politique de Confidentialité
</h1>
</div>
<p className="text-gray-600 text-lg">
Protection des données personnelles
</p>
<p className="text-sm text-gray-500 mt-2">
Dernière mise à jour : 14 octobre 2025
</p>
</div>
</div>
{/* Contenu de la politique */}
<div className="bg-white/80 backdrop-blur-lg border border-white/20 rounded-3xl p-8 shadow-2xl space-y-8">
{/* Section 1 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
1. Présentation
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
La société <strong>Odentas Media SAS</strong>, immatriculée au RCS de Paris sous le numéro 907 880 348,
dont le siège social est situé au 6 rue d'Armaillé, 75017 Paris, éditeur de l'<strong>Espace Paie</strong>,
s'engage à protéger la confidentialité et la sécurité des données personnelles de ses utilisateurs.
</p>
<p>
Cette politique de confidentialité décrit comment nous collectons, utilisons, stockons et protégeons
vos données personnelles dans le cadre de l'utilisation de notre plateforme de gestion de paie et
de contrats pour le secteur du spectacle.
</p>
</div>
</section>
{/* Section 2 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Database className="w-6 h-6 text-blue-600" />
2. Données collectées
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-4">
<p>
Dans le cadre de nos services de gestion de paie et de contrats, nous collectons les catégories
de données suivantes :
</p>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
<h3 className="font-semibold text-gray-800 mb-2">Données d'identification des entreprises clientes :</h3>
<ul className="list-disc list-inside space-y-1 text-sm">
<li>Raison sociale, SIRET, forme juridique</li>
<li>Adresse du siège social</li>
<li>Coordonnées de contact (email, téléphone)</li>
<li>Informations de facturation et comptables</li>
</ul>
</div>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
<h3 className="font-semibold text-gray-800 mb-2">Données des utilisateurs (gestionnaires et administrateurs) :</h3>
<ul className="list-disc list-inside space-y-1 text-sm">
<li>Nom, prénom, email professionnel</li>
<li>Rôle et permissions dans l'application</li>
<li>Logs de connexion et d'activité</li>
</ul>
</div>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
<h3 className="font-semibold text-gray-800 mb-2">Données des salariés (intermittents du spectacle) :</h3>
<ul className="list-disc list-inside space-y-1 text-sm">
<li>État civil : nom, prénom, date et lieu de naissance</li>
<li>Numéro de sécurité sociale, coordonnées</li>
<li>RIB pour les virements de salaire</li>
<li>Contrats de travail (CDDU), bulletins de paie</li>
<li>Documents administratifs (attestations médicales, pièces d'identité, etc.)</li>
<li>Informations relatives aux cotisations sociales</li>
</ul>
</div>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
<h3 className="font-semibold text-gray-800 mb-2">Données de production :</h3>
<ul className="list-disc list-inside space-y-1 text-sm">
<li>Informations sur les spectacles et productions</li>
<li>Dates et lieux de représentation</li>
<li>Numéros d'appel et planning</li>
</ul>
</div>
</div>
</section>
{/* Section 3 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Eye className="w-6 h-6 text-blue-600" />
3. Finalités du traitement
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>Les données personnelles collectées sont utilisées pour les finalités suivantes :</p>
<ul className="list-disc list-inside space-y-2">
<li><strong>Gestion des contrats de travail :</strong> création, modification, signature électronique et archivage des CDDU</li>
<li><strong>Traitement de la paie :</strong> calcul et édition des bulletins de salaire, virements bancaires</li>
<li><strong>Gestion des cotisations sociales :</strong> déclarations et paiements auprès des organismes (URSSAF, Pôle Emploi, Afdas, etc.)</li>
<li><strong>Facturation :</strong> émission et suivi des factures pour nos services</li>
<li><strong>Communication :</strong> envoi de notifications par email relatives aux contrats, paies et documents</li>
<li><strong>Support client :</strong> assistance technique et accompagnement des utilisateurs</li>
<li><strong>Conformité légale :</strong> respect des obligations légales en matière de droit du travail et de comptabilité</li>
<li><strong>Amélioration du service :</strong> analyse d'utilisation et optimisation de la plateforme</li>
</ul>
</div>
</section>
{/* Section 4 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Lock className="w-6 h-6 text-blue-600" />
4. Base légale et durée de conservation
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Le traitement de vos données personnelles repose sur les bases légales suivantes :
</p>
<ul className="list-disc list-inside space-y-2">
<li><strong>Exécution du contrat :</strong> traitement nécessaire à la fourniture de nos services de gestion de paie</li>
<li><strong>Obligations légales :</strong> respect des obligations en matière de droit du travail, fiscalité et comptabilité</li>
<li><strong>Intérêt légitime :</strong> amélioration de nos services et prévention de la fraude</li>
</ul>
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg mt-4">
<h3 className="font-semibold text-gray-800 mb-2">Durées de conservation :</h3>
<ul className="list-disc list-inside space-y-1 text-sm">
<li><strong>Contrats de travail et bulletins de paie :</strong> 50 ans (obligation légale)</li>
<li><strong>Données comptables et fiscales :</strong> 10 ans</li>
<li><strong>Documents justificatifs :</strong> 5 ans à compter de leur réception</li>
<li><strong>Logs de connexion :</strong> 12 mois</li>
<li><strong>Données de facturation :</strong> 10 ans</li>
</ul>
</div>
</div>
</section>
{/* Section 5 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Database className="w-6 h-6 text-blue-600" />
5. Partage et transfert des données
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Vos données personnelles peuvent être partagées avec les destinataires suivants, dans le strict
cadre de nos missions :
</p>
<ul className="list-disc list-inside space-y-2">
<li><strong>Organismes sociaux :</strong> URSSAF, Pôle Emploi, Afdas, Audiens, caisses de retraite (pour les déclarations obligatoires)</li>
<li><strong>Prestataires techniques :</strong> hébergement (Supabase/AWS France), signature électronique (DocuSeal), génération de PDF, envoi d'emails (AWS SES)</li>
<li><strong>Prestataires de paiement :</strong> GoCardless pour les prélèvements SEPA</li>
<li><strong>Administration fiscale :</strong> dans le cadre de nos obligations déclaratives</li>
</ul>
<div className="bg-green-50 border-l-4 border-green-400 p-4 rounded-lg mt-4">
<p className="text-sm">
<strong>🇫🇷 Hébergement en France :</strong> Toutes vos données sont hébergées sur des serveurs
situés en France (région AWS eu-west-3 à Paris) et ne font l'objet d'aucun transfert en dehors
de l'Union Européenne.
</p>
</div>
<p className="mt-4">
Nous ne vendons ni ne louons vos données personnelles à des tiers. Tous nos prestataires sont
contractuellement tenus de respecter la confidentialité et la sécurité de vos données.
</p>
</div>
</section>
{/* Section 6 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Shield className="w-6 h-6 text-blue-600" />
6. Sécurité des données
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Nous mettons en œuvre des mesures techniques et organisationnelles appropriées pour protéger
vos données contre tout accès non autorisé, perte, destruction ou altération :
</p>
<ul className="list-disc list-inside space-y-2">
<li><strong>Chiffrement :</strong> transmission HTTPS (TLS 1.3) et stockage chiffré des données sensibles</li>
<li><strong>Authentification :</strong> authentification à double facteur (OTP par email) et mots de passe sécurisés</li>
<li><strong>Contrôle d'accès :</strong> système de rôles et permissions granulaires (SUPER_ADMIN, ADMIN, AGENT, COMPTA)</li>
<li><strong>Journalisation :</strong> traçabilité des accès et actions sensibles</li>
<li><strong>Sauvegardes :</strong> sauvegardes automatiques quotidiennes avec rétention de 30 jours</li>
<li><strong>Tests de sécurité :</strong> audits réguliers et mises à jour de sécurité</li>
<li><strong>Sensibilisation :</strong> formation de nos équipes aux bonnes pratiques de sécurité</li>
</ul>
</div>
</section>
{/* Section 7 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
7. Vos droits
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Conformément au Règlement Général sur la Protection des Données (RGPD) et à la loi Informatique et Libertés,
vous disposez des droits suivants concernant vos données personnelles :
</p>
<ul className="list-disc list-inside space-y-2">
<li><strong>Droit d'accès :</strong> obtenir une copie de vos données personnelles</li>
<li><strong>Droit de rectification :</strong> corriger des données inexactes ou incomplètes</li>
<li><strong>Droit à l'effacement :</strong> demander la suppression de vos données (sous réserve de nos obligations légales de conservation)</li>
<li><strong>Droit à la limitation :</strong> demander la limitation du traitement de vos données</li>
<li><strong>Droit d'opposition :</strong> vous opposer au traitement de vos données pour des motifs légitimes</li>
<li><strong>Droit à la portabilité :</strong> recevoir vos données dans un format structuré et lisible par machine</li>
<li><strong>Droit de définir des directives :</strong> définir des directives relatives au sort de vos données après votre décès</li>
</ul>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg mt-4">
<p className="text-sm">
<strong>Pour exercer vos droits :</strong> Vous pouvez nous contacter par email à{" "}
<a href="mailto:paie@odentas.fr" className="text-blue-600 underline hover:text-blue-800">
paie@odentas.fr
</a>
{" "}ou par courrier postal à notre siège social. Nous nous engageons à répondre à votre
demande dans un délai maximum d'un mois.
</p>
</div>
<p className="mt-4">
Vous disposez également du droit d'introduire une réclamation auprès de la Commission Nationale
de l'Informatique et des Libertés (CNIL) si vous estimez que le traitement de vos données
personnelles n'est pas conforme à la réglementation :
</p>
<p className="text-sm">
<strong>CNIL</strong><br />
3 Place de Fontenoy - TSA 80715<br />
75334 PARIS CEDEX 07<br />
Tél : 01 53 73 22 22<br />
<a href="https://www.cnil.fr" target="_blank" rel="noopener noreferrer" className="text-blue-600 underline hover:text-blue-800">
www.cnil.fr
</a>
</p>
</div>
</section>
{/* Section 8 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Mail className="w-6 h-6 text-blue-600" />
8. Cookies et technologies similaires
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Notre plateforme utilise des cookies et technologies similaires pour améliorer votre expérience
utilisateur :
</p>
<ul className="list-disc list-inside space-y-2">
<li><strong>Cookies essentiels :</strong> nécessaires au fonctionnement de l'application (authentification, sécurité, préférences de session)</li>
<li><strong>Cookies analytiques :</strong> mesure d'audience via PostHog (anonymisé) pour améliorer nos services</li>
</ul>
<p>
Vous pouvez à tout moment configurer votre navigateur pour refuser les cookies, mais cela pourrait
affecter le bon fonctionnement de certaines fonctionnalités de la plateforme.
</p>
</div>
</section>
{/* Section 9 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
9. Modifications de la politique
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Nous nous réservons le droit de modifier cette politique de confidentialité à tout moment.
Toute modification sera publiée sur cette page avec une date de mise à jour actualisée.
Les modifications importantes vous seront notifiées par email ou via une notification dans l'application.
</p>
<p>
Nous vous encourageons à consulter régulièrement cette page pour rester informé de la manière
dont nous protégeons vos données personnelles.
</p>
</div>
</section>
{/* Section 10 */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<Mail className="w-6 h-6 text-blue-600" />
10. Contact
</h2>
<div className="prose prose-sm max-w-none text-gray-700 space-y-3">
<p>
Pour toute question relative à cette politique de confidentialité ou au traitement de vos
données personnelles, vous pouvez nous contacter :
</p>
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
<p className="font-semibold text-gray-800 mb-2">Odentas Media SAS</p>
<p className="text-sm space-y-1">
<strong>Adresse :</strong> 6 rue d'Armaillé, 75017 Paris<br />
<strong>Email :</strong>{" "}
<a href="mailto:paie@odentas.fr" className="text-blue-600 underline hover:text-blue-800">
paie@odentas.fr
</a><br />
<strong>RCS Paris :</strong> 907 880 348<br />
<strong>Responsable du traitement :</strong> Renaud BREVIERE-ABRAHAM
</p>
</div>
</div>
</section>
{/* Footer de la politique */}
<div className="border-t border-gray-200 pt-6 text-center">
<p className="text-sm text-gray-500 mb-4">
Cette politique de confidentialité s'applique exclusivement à l'utilisation de l'Espace Paie Odentas.
</p>
<Link
href="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Retour à l'accueil
</Link>
</div>
</div>
{/* Footer */}
<div className="text-center mt-6 text-xs text-gray-500">
<p>© 20212025 Odentas Media SAS RCS Paris 907 880 348</p>
<div className="mt-2 flex items-center justify-center gap-2">
<span role="img" aria-label="Drapeau français">🇫🇷</span>
<span>Données hébergées en France</span>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,12 @@
// app/politique-confidentialite/page.tsx
import { Metadata } from "next";
import PolitiqueConfidentialiteContent from "./PolitiqueConfidentialiteContent";
export const metadata: Metadata = {
title: "Politique de Confidentialité | Espace Paie Odentas",
description: "Politique de confidentialité et de protection des données personnelles de l'Espace Paie Odentas",
};
export default function PolitiqueConfidentialite() {
return <PolitiqueConfidentialiteContent />;
}

View file

@ -0,0 +1,98 @@
"use client";
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ShieldCheck, X } from "lucide-react";
export default function PopupInfoSuivi({
policyUrl = "/politique-confidentialite",
storageKey = "odentas_info_suivi_ack_v1",
}: {
policyUrl?: string;
storageKey?: string;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
const alreadyAck = typeof window !== "undefined" && localStorage.getItem(storageKey);
if (!alreadyAck) {
const t = setTimeout(() => setOpen(true), 1200);
return () => clearTimeout(t);
}
}, [storageKey]);
const acknowledge = () => {
try {
localStorage.setItem(storageKey, "1");
} catch {}
setOpen(false);
};
return (
<AnimatePresence>
{open && (
<motion.aside
role="dialog"
aria-live="polite"
aria-label="Information sur la confidentialité et le suivi"
initial={{ opacity: 0, y: 24, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 24, scale: 0.98 }}
transition={{ type: "spring", stiffness: 380, damping: 28 }}
className="fixed left-4 bottom-4 z-[60] max-w-sm w-[92vw] sm:w-96"
>
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 backdrop-blur shadow-lg">
<button
type="button"
aria-label="Fermer"
onClick={() => setOpen(false)}
className="absolute right-2 top-2 inline-flex items-center justify-center rounded-full p-1.5 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-sky-400 transition-colors"
>
<X className="h-4 w-4 text-slate-500" />
</button>
<div className="flex items-start gap-3 p-4 pr-10">
<div className="mt-0.5 shrink-0 rounded-xl bg-gradient-to-br from-sky-100 to-blue-100 p-2">
<ShieldCheck className="h-5 w-5 text-sky-700" />
</div>
<div className="text-sm leading-snug text-slate-700">
<p className="font-semibold text-slate-900 mb-1.5">Transparence & confidentialité</p>
<p>
L'Espace Paie Odentas utilise des cookies essentiels pour votre authentification et votre sécurité,
ainsi qu'un outil d'analyse (PostHog) pour améliorer nos services.
Vos données sont hébergées en France 🇫🇷 et ne sont jamais revendues.
En utilisant l'Espace Paie, vous acceptez notre{" "}
<a
href={policyUrl}
className="underline underline-offset-2 decoration-sky-400 hover:decoration-sky-600 text-sky-700 font-medium transition-colors"
target="_blank"
rel="noreferrer noopener"
>
Politique de Confidentialité
</a>
.
</p>
<div className="mt-3 flex items-center gap-2">
<button
onClick={acknowledge}
className="inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm font-medium shadow-sm border border-sky-200 bg-sky-50 hover:bg-sky-100 text-sky-900 focus:outline-none focus:ring-2 focus:ring-sky-400 transition-colors"
>
J'ai compris
</button>
<a
href={policyUrl}
target="_blank"
rel="noreferrer noopener"
className="text-sm text-slate-600 hover:text-slate-900 underline underline-offset-2 transition-colors"
>
En savoir plus
</a>
</div>
</div>
</div>
</div>
</motion.aside>
)}
</AnimatePresence>
);
}

View file

@ -1,9 +1,39 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { TicketReplyConfirmationModal } from "./TicketReplyConfirmationModal";
export default function StaffTicketActions({ ticketId, status, mode = "both" }: { ticketId: string; status: string; mode?: "status" | "message" | "both" }) {
const [updating, setUpdating] = useState(false);
const [sending, setSending] = useState(false);
const [showModal, setShowModal] = useState(false);
const [pendingMessage, setPendingMessage] = useState({ body: "", internal: false });
const [recipientInfo, setRecipientInfo] = useState<{ email: string; name: string } | null>(null);
// Récupérer les informations du destinataire
useEffect(() => {
async function fetchRecipientInfo() {
try {
console.log('🔍 [StaffTicketActions] Fetching recipient info for ticket:', ticketId);
const url = `/api/tickets/${ticketId}/recipient-info`;
console.log('🔍 [StaffTicketActions] URL:', url);
const res = await fetch(url, { credentials: 'include' });
console.log('🔍 [StaffTicketActions] Response status:', res.status);
if (res.ok) {
const data = await res.json();
console.log('✅ [StaffTicketActions] Recipient info:', data);
setRecipientInfo(data);
} else {
const errorText = await res.text();
console.error('❌ [StaffTicketActions] Error response:', res.status, errorText);
}
} catch (e) {
console.error('❌ [StaffTicketActions] Erreur lors de la récupération des infos destinataire:', e);
}
}
fetchRecipientInfo();
}, [ticketId]);
async function onStatusSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
@ -27,20 +57,46 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }:
const fd = new FormData(form);
const body = String(fd.get('body') || '').trim();
const internal = fd.get('internal') === 'on';
if (!body) return;
// Si c'est une note interne, envoyer directement
if (internal) {
await sendMessage(body, internal, form);
} else {
// Sinon, afficher le modal de confirmation
setPendingMessage({ body, internal });
setShowModal(true);
}
}
async function sendMessage(body: string, internal: boolean, formToReset?: HTMLFormElement) {
setSending(true);
try {
const res = await fetch(`/api/tickets/${ticketId}/messages`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body, internal }) });
const res = await fetch(`/api/tickets/${ticketId}/messages`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body, internal })
});
if (!res.ok) throw new Error(await res.text());
form.reset();
if (formToReset) formToReset.reset();
location.reload();
} catch (e: any) {
alert('Erreur: ' + (e?.message || 'inconnue'));
} finally {
setSending(false);
setShowModal(false);
}
}
function handleConfirmSend() {
// Récupérer le formulaire pour le reset
const form = document.querySelector('form[data-ticket-form]') as HTMLFormElement;
sendMessage(pendingMessage.body, pendingMessage.internal, form);
}
return (
<>
{(mode === 'status' || mode === 'both') && (
@ -56,7 +112,7 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }:
)}
{(mode === 'message' || mode === 'both') && (
<form className="space-y-2" onSubmit={onMessageSubmit}>
<form className="space-y-2" onSubmit={onMessageSubmit} data-ticket-form>
<textarea name="body" className="w-full px-3 py-2 rounded-lg border bg-transparent text-sm min-h-[120px]" placeholder="Réponse…" />
<div className="flex items-center gap-3">
<label className="inline-flex items-center gap-2 text-sm"><input type="checkbox" name="internal" /> Note interne</label>
@ -64,6 +120,19 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }:
</div>
</form>
)}
{/* Modal de confirmation */}
{recipientInfo && (
<TicketReplyConfirmationModal
isOpen={showModal}
recipientEmail={recipientInfo.email}
recipientName={recipientInfo.name}
messageBody={pendingMessage.body}
onConfirm={handleConfirmSend}
onCancel={() => setShowModal(false)}
isLoading={sending}
/>
)}
</>
);
}

View file

@ -0,0 +1,87 @@
"use client";
import { Mail, Send, X } from "lucide-react";
import { Button } from "../ui/button";
interface TicketReplyConfirmationModalProps {
isOpen: boolean;
recipientEmail: string;
recipientName: string;
messageBody: string;
onConfirm: () => void;
onCancel: () => void;
isLoading?: boolean;
}
export function TicketReplyConfirmationModal({
isOpen,
recipientEmail,
recipientName,
messageBody,
onConfirm,
onCancel,
isLoading = false
}: TicketReplyConfirmationModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center">
<div className="bg-white rounded-2xl max-w-2xl w-full mx-4 shadow-2xl border max-h-[90vh] flex flex-col">
<div className="p-6 flex-1 overflow-y-auto">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<Mail className="w-5 h-5 text-blue-600" />
</div>
<h2 className="text-lg font-semibold text-slate-900">Confirmation d'envoi</h2>
</div>
<div className="space-y-4 mb-6">
<div className="rounded-lg border bg-slate-50 p-4">
<div className="text-sm text-slate-500 mb-1">Destinataire</div>
<div className="font-medium text-slate-900">{recipientName}</div>
<div className="text-sm text-slate-600 flex items-center gap-2 mt-1">
<Mail className="w-4 h-4" />
{recipientEmail}
</div>
</div>
<div className="rounded-lg border bg-white p-4">
<div className="text-sm text-slate-500 mb-2">Votre message</div>
<div className="text-sm text-slate-900 whitespace-pre-wrap bg-slate-50 p-3 rounded border max-h-[200px] overflow-y-auto">
{messageBody}
</div>
</div>
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
<div className="text-sm text-blue-900">
<strong>Note :</strong> Un email sera envoyé à l'adresse personnelle de l'utilisateur avec votre réponse.
</div>
</div>
</div>
</div>
<div className="border-t p-4 bg-slate-50 rounded-b-2xl">
<div className="flex gap-3">
<Button
variant="outline"
onClick={onCancel}
className="flex-1"
disabled={isLoading}
>
<X className="w-4 h-4 mr-2" />
Annuler
</Button>
<Button
onClick={onConfirm}
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white"
disabled={isLoading}
>
<Send className="w-4 h-4 mr-2" />
{isLoading ? "Envoi en cours..." : "Envoyer la réponse"}
</Button>
</div>
</div>
</div>
</div>
);
}

View file

@ -432,16 +432,133 @@ export async function sendTwoFaEnabledEmail(
const emailData: EmailDataV2 = {
firstName: data.firstName,
userEmail: toEmail,
status: '2FA activée',
status: '2FA désactivée',
eventDate: data.eventDate || new Date().toLocaleString('fr-FR'),
platform: data.platform || 'Odentas Paie',
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/compte/securite`,
};
await sendUniversalEmailV2({
type: 'twofa-enabled',
type: 'twofa-disabled',
toEmail,
subject: '2FA activée sur votre compte',
subject: '2FA désactivée sur votre compte',
data: emailData,
});
}
/**
* Support: Notification interne de création de ticket
*/
export async function sendInternalTicketCreatedEmail(
data: {
ticketId: string;
ticketSubject: string;
ticketCategory: string;
ticketMessage: string;
userName: string;
userEmail: string;
organizationName?: string;
employerCode?: string;
}
) {
// Convertir les sauts de ligne en <br> pour l'affichage HTML
const formattedMessage = data.ticketMessage.replace(/\n/g, '<br>');
const emailData: EmailDataV2 = {
ticketId: data.ticketId,
ticketSubject: data.ticketSubject,
ticketCategory: data.ticketCategory,
ticketMessage: formattedMessage,
userName: data.userName,
userEmail: data.userEmail,
organizationName: data.organizationName || 'Non définie',
employerCode: data.employerCode || 'Non défini',
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/staff/tickets/${data.ticketId}`,
};
await sendUniversalEmailV2({
type: 'support-ticket-created',
toEmail: 'paie@odentas.fr',
subject: `[SUPPORT] Nouveau ticket : ${data.ticketSubject}`,
data: emailData,
});
}
/**
* Support: Notification interne de réponse utilisateur
*/
export async function sendInternalTicketReplyEmail(
data: {
ticketId: string;
ticketSubject: string;
ticketStatus: string;
userMessage: string;
userName: string;
userEmail: string;
organizationName?: string;
employerCode?: string;
}
) {
// Convertir les sauts de ligne en <br> pour l'affichage HTML
const formattedMessage = data.userMessage.replace(/\n/g, '<br>');
const emailData: EmailDataV2 = {
ticketId: data.ticketId,
ticketSubject: data.ticketSubject,
ticketStatus: data.ticketStatus,
userMessage: formattedMessage,
userName: data.userName,
userEmail: data.userEmail,
organizationName: data.organizationName || 'Non définie',
employerCode: data.employerCode || 'Non défini',
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/staff/tickets/${data.ticketId}`,
};
await sendUniversalEmailV2({
type: 'support-ticket-reply',
toEmail: 'paie@odentas.fr',
subject: `[SUPPORT] Réponse au ticket : ${data.ticketSubject}`,
data: emailData,
});
}
/**
* Support: Notification de réponse à un ticket
*/
export async function sendSupportReplyEmail(
toEmail: string,
data: {
firstName?: string;
ticketId: string;
ticketSubject: string;
staffName: string;
staffMessage: string;
organizationName?: string;
employerCode?: string;
}
) {
// Formater le nom du staff : première lettre en majuscule + [Staff Odentas]
const formattedStaffName = data.staffName.charAt(0).toUpperCase() + data.staffName.slice(1) + ' [Staff Odentas]';
// Convertir les sauts de ligne en <br> pour l'affichage HTML
const formattedMessage = data.staffMessage.replace(/\n/g, '<br>');
const emailData: EmailDataV2 = {
firstName: data.firstName,
ticketId: data.ticketId,
ticketSubject: data.ticketSubject,
staffName: formattedStaffName,
staffMessage: formattedMessage,
organizationName: data.organizationName || 'Non définie',
employerCode: data.employerCode || 'Non défini',
handlerName: 'Renaud BREVIERE-ABRAHAM',
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/support/${data.ticketId}`,
};
await sendUniversalEmailV2({
type: 'support-reply',
toEmail,
subject: `Nouvelle réponse à votre ticket: ${data.ticketSubject}`,
data: emailData,
});
}

View file

@ -38,6 +38,10 @@ export type EmailTypeV2 =
| 'salary-transfer-notification' // Nouveau type pour notification d'appel à virement
| 'contribution-notification' // Nouveau type pour notification de cotisations
| 'notification'
// Support
| 'support-reply' // Réponse du staff à un ticket support
| 'support-ticket-created' // Notification interne : nouveau ticket créé
| 'support-ticket-reply' // Notification interne : réponse utilisateur à un ticket
// Accès / habilitations
| 'account-activation'
| 'access-updated'
@ -80,6 +84,17 @@ export interface EmailDataV2 {
periodLabel?: string;
deadline?: string;
transferReference?: string;
// Ajout des champs pour les tickets support
ticketId?: string;
ticketSubject?: string;
staffName?: string;
staffMessage?: string;
// Ajout des champs pour les notifications internes de tickets
ticketCategory?: string;
ticketMessage?: string;
ticketStatus?: string;
userEmail?: string;
userMessage?: string;
[key: string]: unknown;
}
@ -809,6 +824,106 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
cardBorder: '#E5E7EB',
cardTitleColor: '#374151'
}
},
'support-reply': {
subject: 'Nouvelle réponse à votre ticket support',
title: 'Réponse à votre ticket',
greeting: '{{#if firstName}}Bonjour {{firstName}},{{/if}}',
mainMessage: 'Vous avez reçu une réponse à votre ticket support.',
ctaText: 'Voir le ticket',
footerText: 'Vous recevez cet e-mail suite à une réponse de notre équipe support.',
preheaderText: 'Notre équipe support a répondu à votre ticket',
colors: {
headerColor: STANDARD_COLORS.HEADER,
titleColor: '#0F172A',
buttonColor: STANDARD_COLORS.BUTTON,
buttonTextColor: STANDARD_COLORS.BUTTON_TEXT,
cardBackgroundColor: '#FFFFFF',
cardBorder: '#E5E7EB',
cardTitleColor: '#0F172A',
},
infoCard: [
{ label: 'Votre structure', key: 'organizationName' },
{ label: 'Votre code employeur', key: 'employerCode' },
{ label: 'Votre gestionnaire', key: 'handlerName' },
],
detailsCard: {
title: 'Message',
rows: [
{ label: 'Répondu par', key: 'staffName' },
{ label: 'Sujet', key: 'ticketSubject' },
{ label: 'Réponse', key: 'staffMessage' },
]
}
},
'support-ticket-created': {
subject: '[SUPPORT] Nouveau ticket : {{ticketSubject}}',
title: '🎫 Nouveau ticket support',
greeting: 'Notification interne',
mainMessage: 'Un utilisateur a créé un nouveau ticket support.',
ctaText: 'Voir le ticket',
footerText: 'Notification automatique du système de support Odentas.',
preheaderText: 'Nouveau ticket support créé',
colors: {
headerColor: STANDARD_COLORS.HEADER,
titleColor: '#0F172A',
buttonColor: '#3B82F6', // Bleu pour les notifications internes
buttonTextColor: '#FFFFFF',
cardBackgroundColor: '#FFFFFF',
cardBorder: '#E5E7EB',
cardTitleColor: '#0F172A',
},
infoCard: [
{ label: 'Organisation', key: 'organizationName' },
{ label: 'Code employeur', key: 'employerCode' },
{ label: 'Créé par', key: 'userName' },
{ label: 'Email', key: 'userEmail' },
],
detailsCard: {
title: 'Détails du ticket',
rows: [
{ label: 'ID', key: 'ticketId' },
{ label: 'Sujet', key: 'ticketSubject' },
{ label: 'Catégorie', key: 'ticketCategory' },
{ label: 'Message', key: 'ticketMessage' },
]
}
},
'support-ticket-reply': {
subject: '[SUPPORT] Réponse au ticket : {{ticketSubject}}',
title: '💬 Réponse sur un ticket',
greeting: 'Notification interne',
mainMessage: 'Un utilisateur a répondu à un ticket support.',
ctaText: 'Voir le ticket',
footerText: 'Notification automatique du système de support Odentas.',
preheaderText: 'Nouvelle réponse sur un ticket support',
colors: {
headerColor: STANDARD_COLORS.HEADER,
titleColor: '#0F172A',
buttonColor: '#3B82F6', // Bleu pour les notifications internes
buttonTextColor: '#FFFFFF',
cardBackgroundColor: '#FFFFFF',
cardBorder: '#E5E7EB',
cardTitleColor: '#0F172A',
},
infoCard: [
{ label: 'Organisation', key: 'organizationName' },
{ label: 'Code employeur', key: 'employerCode' },
{ label: 'Répondu par', key: 'userName' },
{ label: 'Email', key: 'userEmail' },
],
detailsCard: {
title: 'Détails de la réponse',
rows: [
{ label: 'ID', key: 'ticketId' },
{ label: 'Sujet', key: 'ticketSubject' },
{ label: 'Statut', key: 'ticketStatus' },
{ label: 'Réponse', key: 'userMessage' },
]
}
}
};

View file

@ -49,12 +49,16 @@ export async function middleware(req: NextRequest) {
// 2) Vérification du mode maintenance
// Éviter de vérifier sur les API calls, assets, la page maintenance elle-même, la page de connexion,
// et les pages publiques (auto-declaration, dl-contrat-signe, signature-salarie)
// et les pages publiques (auto-declaration, dl-contrat-signe, signature-salarie, politique-confidentialite, mentions-legales)
const isApiOrAssets = path.startsWith('/api') || path.startsWith('/_next') ||
path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile;
const isMaintenancePage = path === '/maintenance';
const isSigninPage = path === '/signin';
const isPublicPage = path.startsWith('/auto-declaration') || path.startsWith('/dl-contrat-signe') || path.startsWith('/signature-salarie');
const isPublicPage = path.startsWith('/auto-declaration') ||
path.startsWith('/dl-contrat-signe') ||
path.startsWith('/signature-salarie') ||
path === '/politique-confidentialite' ||
path === '/mentions-legales';
// Ne pas impacter l'environnement local/dev par le mode maintenance
const hostname = req.nextUrl.hostname || '';

43
package-lock.json generated
View file

@ -23,6 +23,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.2.2",
"framer-motion": "^12.23.24",
"html2canvas": "^1.4.1",
"js-cookie": "^3.0.5",
"lucide-react": "^0.460.0",
@ -5728,6 +5729,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -6977,6 +7005,21 @@
"obliterator": "^1.6.1"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -24,6 +24,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.2.2",
"framer-motion": "^12.23.24",
"html2canvas": "^1.4.1",
"js-cookie": "^3.0.5",
"lucide-react": "^0.460.0",

View file

@ -68,7 +68,7 @@
{{#each detailsCardRows}}
<div style="margin-bottom:{{#unless @last}}12px{{else}}0{{/unless}};">
<div class="label" style="font-size:12px; color:#64748B; text-transform:uppercase; letter-spacing:0.03em; margin-bottom:6px; font-weight:500;">{{this.label}}</div>
<div class="value" style="font-size:15px; color:#0F172A; font-weight:500; line-height:1.3;">{{this.value}}</div>
<div class="value" style="font-size:15px; color:#0F172A; font-weight:500; line-height:1.3;">{{{this.value}}}</div>
</div>
{{/each}}
</div>

104
test-ticket-notifications.sh Executable file
View file

@ -0,0 +1,104 @@
#!/bin/bash
# Script de test pour les notifications de tickets support
# Ce script vérifie que tous les fichiers sont en place et correctement configurés
echo "🔍 Vérification des fichiers..."
echo ""
# Couleurs
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Fonction de vérification
check_file() {
if [ -f "$1" ]; then
echo -e "${GREEN}${NC} $1"
return 0
else
echo -e "${RED}${NC} $1"
return 1
fi
}
# Vérifier les fichiers
echo "📁 Vérification de la structure des fichiers API:"
check_file "app/api/tickets/[id]/recipient-info/route.ts"
check_file "app/api/tickets/[id]/messages/route.ts"
echo ""
echo "📁 Vérification des composants:"
check_file "components/staff/StaffTicketActions.tsx"
check_file "components/staff/TicketReplyConfirmationModal.tsx"
echo ""
echo "📁 Vérification des pages:"
check_file "app/(app)/staff/tickets/[id]/page.tsx"
echo ""
echo "📁 Vérification des helpers:"
check_file "lib/emailMigrationHelpers.ts"
check_file "lib/emailTemplateService.ts"
echo ""
echo "🔍 Vérification du contenu de recipient-info/route.ts..."
if grep -q "display_name" "app/api/tickets/[id]/recipient-info/route.ts"; then
echo -e "${GREEN}${NC} Le code vérifie bien 'display_name' en priorité"
else
echo -e "${RED}${NC} Le code ne vérifie pas 'display_name'"
fi
if grep -q "console.log.*recipient-info" "app/api/tickets/[id]/recipient-info/route.ts"; then
echo -e "${GREEN}${NC} Les logs de debug sont présents"
else
echo -e "${YELLOW}⚠️${NC} Les logs de debug sont absents"
fi
echo ""
echo "🔍 Vérification du contenu de page.tsx..."
if grep -q "display_name" "app/(app)/staff/tickets/[id]/page.tsx"; then
echo -e "${GREEN}${NC} Le code vérifie bien 'display_name' en priorité"
else
echo -e "${RED}${NC} Le code ne vérifie pas 'display_name'"
fi
if grep -q "console.log.*ticket page" "app/(app)/staff/tickets/[id]/page.tsx"; then
echo -e "${GREEN}${NC} Les logs de debug sont présents"
else
echo -e "${YELLOW}⚠️${NC} Les logs de debug sont absents"
fi
echo ""
echo "🔍 Vérification du contenu de StaffTicketActions.tsx..."
if grep -q "recipient-info" "components/staff/StaffTicketActions.tsx"; then
echo -e "${GREEN}${NC} Le composant appelle bien l'API recipient-info"
else
echo -e "${RED}${NC} Le composant n'appelle pas l'API recipient-info"
fi
if grep -q "console.log.*StaffTicketActions" "components/staff/StaffTicketActions.tsx"; then
echo -e "${GREEN}${NC} Les logs de debug sont présents"
else
echo -e "${YELLOW}⚠️${NC} Les logs de debug sont absents"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo -e "${YELLOW}📝 Pour tester:${NC}"
echo "1. Redémarrer le serveur: npm run dev"
echo "2. Ouvrir la console du navigateur (F12)"
echo "3. Aller sur une page /staff/tickets/[id]"
echo "4. Regarder les logs dans la console navigateur ET dans le terminal"
echo ""
echo -e "${YELLOW}🔍 Logs à surveiller dans le terminal:${NC}"
echo " 📋 [ticket page] User metadata: {...}"
echo " 📋 [recipient-info] User metadata: {...}"
echo ""
echo -e "${YELLOW}🔍 Logs à surveiller dans la console navigateur:${NC}"
echo " 🔍 [StaffTicketActions] Fetching recipient info for ticket: ..."
echo " ✅ [StaffTicketActions] Recipient info: {...}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"