Système notif tickets support
This commit is contained in:
parent
f460c1da5a
commit
5e0997ede8
30 changed files with 3942 additions and 15 deletions
169
DEBUG_EMAIL_NOTIFICATIONS_TICKETS.md
Normal file
169
DEBUG_EMAIL_NOTIFICATIONS_TICKETS.md
Normal 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*
|
||||||
189
EMAIL_SUPPORT_REPLY_IMPROVEMENTS.md
Normal file
189
EMAIL_SUPPORT_REPLY_IMPROVEMENTS.md
Normal 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
212
GUIDE_DEMARRAGE_TICKETS.md
Normal 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
137
PAGES_LEGALES.md
Normal 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
188
POPUP_INFO_SUIVI.md
Normal 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/)
|
||||||
125
RESOLUTION_FINALE_TICKETS.md
Normal file
125
RESOLUTION_FINALE_TICKETS.md
Normal 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*
|
||||||
300
SUPPORT_INTERNAL_NOTIFICATIONS.md
Normal file
300
SUPPORT_INTERNAL_NOTIFICATIONS.md
Normal 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
|
||||||
65
SUPPORT_INTERNAL_NOTIFICATIONS_SUMMARY.md
Normal file
65
SUPPORT_INTERNAL_NOTIFICATIONS_SUMMARY.md
Normal 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)
|
||||||
214
SUPPORT_TICKET_NOTIFICATIONS.md
Normal file
214
SUPPORT_TICKET_NOTIFICATIONS.md
Normal 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
|
||||||
371
SUPPORT_TICKET_NOTIFICATIONS_FIXES.md
Normal file
371
SUPPORT_TICKET_NOTIFICATIONS_FIXES.md
Normal 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*
|
||||||
112
TICKET_CREATOR_INFO_DISPLAY.md
Normal file
112
TICKET_CREATOR_INFO_DISPLAY.md
Normal 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*
|
||||||
|
|
@ -1,11 +1,32 @@
|
||||||
export const dynamic = "force-dynamic";
|
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) {
|
function formatDate(d?: string | null) {
|
||||||
if (!d) return "—";
|
if (!d) return "—";
|
||||||
try { return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(d)); } catch { return d as string; }
|
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 StaffTicketActions from "@/components/staff/StaffTicketActions";
|
||||||
import MarkTicketRead from "@/components/staff/MarkTicketRead";
|
import MarkTicketRead from "@/components/staff/MarkTicketRead";
|
||||||
|
|
||||||
|
|
@ -25,6 +46,80 @@ export default async function StaffTicketDetail({ params }: { params: { id: stri
|
||||||
.maybeSingle();
|
.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>);
|
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
|
const { data: messages } = await sb
|
||||||
.from("ticket_messages")
|
.from("ticket_messages")
|
||||||
.select("id, ticket_id, author_id, body, internal, via, created_at")
|
.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" />
|
<StaffTicketActions ticketId={ticket.id} status={ticket.status} mode="status" />
|
||||||
</div>
|
</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="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">
|
<div className="rounded-2xl border bg-white p-4 space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { sendSupportReplyEmail, sendInternalTicketReplyEmail } from "@/lib/emailMigrationHelpers";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(_: Request, { params }: { params: { id: string } }) {
|
export async function GET(_: Request, { params }: { params: { id: string } }) {
|
||||||
const sb = createRouteHandlerClient({ cookies });
|
const sb = createSbServer();
|
||||||
|
|
||||||
// Récupérer les messages
|
// Récupérer les messages
|
||||||
const { data: messages, error } = await sb
|
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 } }) {
|
export async function POST(req: Request, { params }: { params: { id: string } }) {
|
||||||
const sb = createRouteHandlerClient({ cookies });
|
const sb = createSbServer();
|
||||||
const { data: { user } } = await sb.auth.getUser();
|
const { data: { user } } = await sb.auth.getUser();
|
||||||
if (!user) return new NextResponse("Unauthorized", { status: 401 });
|
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 });
|
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 });
|
return NextResponse.json({ ok: true }, { status: 201 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
86
app/api/tickets/[id]/recipient-info/route.ts
Normal file
86
app/api/tickets/[id]/recipient-info/route.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
|
import { sendInternalTicketCreatedEmail } from "@/lib/emailMigrationHelpers";
|
||||||
|
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
|
@ -94,6 +96,53 @@ export async function POST(req: Request) {
|
||||||
.update({ message_count: 1 })
|
.update({ message_count: 1 })
|
||||||
.eq("id", ticket.id);
|
.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 });
|
return NextResponse.json(ticket, { status: 201 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Providers from "@/components/Providers";
|
||||||
import ProgressBar from "@/components/ProgressBar";
|
import ProgressBar from "@/components/ProgressBar";
|
||||||
import { PostHogPageView } from "@/components/PostHogPageView";
|
import { PostHogPageView } from "@/components/PostHogPageView";
|
||||||
import PostHogIdentifier from "@/components/PostHogIdentifier";
|
import PostHogIdentifier from "@/components/PostHogIdentifier";
|
||||||
|
import PopupInfoSuivi from "@/components/PopupInfoSuivi";
|
||||||
import { useEffect, Suspense } from "react";
|
import { useEffect, Suspense } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,6 +73,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||||
<PostHogIdentifier />
|
<PostHogIdentifier />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{children}
|
{children}
|
||||||
|
{/* Popup d'information sur la confidentialité et le suivi */}
|
||||||
|
<PopupInfoSuivi policyUrl="/politique-confidentialite" />
|
||||||
</Providers>
|
</Providers>
|
||||||
{/* BugReporter temporairement masqué */}
|
{/* BugReporter temporairement masqué */}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
407
app/mentions-legales/MentionsLegalesContent.tsx
Normal file
407
app/mentions-legales/MentionsLegalesContent.tsx
Normal 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>© 2021–2025 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/mentions-legales/page.tsx
Normal file
12
app/mentions-legales/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
|
|
@ -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>© 2021–2025 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/politique-confidentialite/page.tsx
Normal file
12
app/politique-confidentialite/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
98
components/PopupInfoSuivi.tsx
Normal file
98
components/PopupInfoSuivi.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,39 @@
|
||||||
"use client";
|
"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" }) {
|
export default function StaffTicketActions({ ticketId, status, mode = "both" }: { ticketId: string; status: string; mode?: "status" | "message" | "both" }) {
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
const [sending, setSending] = 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>) {
|
async function onStatusSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -27,20 +57,46 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }:
|
||||||
const fd = new FormData(form);
|
const fd = new FormData(form);
|
||||||
const body = String(fd.get('body') || '').trim();
|
const body = String(fd.get('body') || '').trim();
|
||||||
const internal = fd.get('internal') === 'on';
|
const internal = fd.get('internal') === 'on';
|
||||||
|
|
||||||
if (!body) return;
|
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);
|
setSending(true);
|
||||||
try {
|
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());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
form.reset();
|
|
||||||
|
if (formToReset) formToReset.reset();
|
||||||
location.reload();
|
location.reload();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert('Erreur: ' + (e?.message || 'inconnue'));
|
alert('Erreur: ' + (e?.message || 'inconnue'));
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{(mode === 'status' || mode === 'both') && (
|
{(mode === 'status' || mode === 'both') && (
|
||||||
|
|
@ -56,7 +112,7 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }:
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(mode === 'message' || 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…" />
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal de confirmation */}
|
||||||
|
{recipientInfo && (
|
||||||
|
<TicketReplyConfirmationModal
|
||||||
|
isOpen={showModal}
|
||||||
|
recipientEmail={recipientInfo.email}
|
||||||
|
recipientName={recipientInfo.name}
|
||||||
|
messageBody={pendingMessage.body}
|
||||||
|
onConfirm={handleConfirmSend}
|
||||||
|
onCancel={() => setShowModal(false)}
|
||||||
|
isLoading={sending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
components/staff/TicketReplyConfirmationModal.tsx
Normal file
87
components/staff/TicketReplyConfirmationModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -432,16 +432,133 @@ export async function sendTwoFaEnabledEmail(
|
||||||
const emailData: EmailDataV2 = {
|
const emailData: EmailDataV2 = {
|
||||||
firstName: data.firstName,
|
firstName: data.firstName,
|
||||||
userEmail: toEmail,
|
userEmail: toEmail,
|
||||||
status: '2FA activée',
|
status: '2FA désactivée',
|
||||||
eventDate: data.eventDate || new Date().toLocaleString('fr-FR'),
|
eventDate: data.eventDate || new Date().toLocaleString('fr-FR'),
|
||||||
platform: data.platform || 'Odentas Paie',
|
platform: data.platform || 'Odentas Paie',
|
||||||
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/compte/securite`,
|
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/compte/securite`,
|
||||||
};
|
};
|
||||||
|
|
||||||
await sendUniversalEmailV2({
|
await sendUniversalEmailV2({
|
||||||
type: 'twofa-enabled',
|
type: 'twofa-disabled',
|
||||||
toEmail,
|
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,
|
data: emailData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ export type EmailTypeV2 =
|
||||||
| 'salary-transfer-notification' // Nouveau type pour notification d'appel à virement
|
| 'salary-transfer-notification' // Nouveau type pour notification d'appel à virement
|
||||||
| 'contribution-notification' // Nouveau type pour notification de cotisations
|
| 'contribution-notification' // Nouveau type pour notification de cotisations
|
||||||
| 'notification'
|
| '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
|
// Accès / habilitations
|
||||||
| 'account-activation'
|
| 'account-activation'
|
||||||
| 'access-updated'
|
| 'access-updated'
|
||||||
|
|
@ -80,6 +84,17 @@ export interface EmailDataV2 {
|
||||||
periodLabel?: string;
|
periodLabel?: string;
|
||||||
deadline?: string;
|
deadline?: string;
|
||||||
transferReference?: 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;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -809,6 +824,106 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
|
||||||
cardBorder: '#E5E7EB',
|
cardBorder: '#E5E7EB',
|
||||||
cardTitleColor: '#374151'
|
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' },
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,16 @@ export async function middleware(req: NextRequest) {
|
||||||
|
|
||||||
// 2) Vérification du mode maintenance
|
// 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,
|
// É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') ||
|
const isApiOrAssets = path.startsWith('/api') || path.startsWith('/_next') ||
|
||||||
path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile;
|
path.startsWith('/favicon') || path.startsWith('/public') || isStaticFile;
|
||||||
const isMaintenancePage = path === '/maintenance';
|
const isMaintenancePage = path === '/maintenance';
|
||||||
const isSigninPage = path === '/signin';
|
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
|
// Ne pas impacter l'environnement local/dev par le mode maintenance
|
||||||
const hostname = req.nextUrl.hostname || '';
|
const hostname = req.nextUrl.hostname || '';
|
||||||
|
|
|
||||||
43
package-lock.json
generated
43
package-lock.json
generated
|
|
@ -23,6 +23,7 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
|
|
@ -5728,6 +5729,33 @@
|
||||||
"url": "https://github.com/sponsors/rawify"
|
"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": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
|
@ -6977,6 +7005,21 @@
|
||||||
"obliterator": "^1.6.1"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
{{#each detailsCardRows}}
|
{{#each detailsCardRows}}
|
||||||
<div style="margin-bottom:{{#unless @last}}12px{{else}}0{{/unless}};">
|
<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="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>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
104
test-ticket-notifications.sh
Executable file
104
test-ticket-notifications.sh
Executable 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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
Loading…
Reference in a new issue