diff --git a/DEBUG_EMAIL_NOTIFICATIONS_TICKETS.md b/DEBUG_EMAIL_NOTIFICATIONS_TICKETS.md new file mode 100644 index 0000000..667d268 --- /dev/null +++ b/DEBUG_EMAIL_NOTIFICATIONS_TICKETS.md @@ -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* diff --git a/EMAIL_SUPPORT_REPLY_IMPROVEMENTS.md b/EMAIL_SUPPORT_REPLY_IMPROVEMENTS.md new file mode 100644 index 0000000..5bbd4db --- /dev/null +++ b/EMAIL_SUPPORT_REPLY_IMPROVEMENTS.md @@ -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* diff --git a/GUIDE_DEMARRAGE_TICKETS.md b/GUIDE_DEMARRAGE_TICKETS.md new file mode 100644 index 0000000..8c5612c --- /dev/null +++ b/GUIDE_DEMARRAGE_TICKETS.md @@ -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* diff --git a/PAGES_LEGALES.md b/PAGES_LEGALES.md new file mode 100644 index 0000000..58c557d --- /dev/null +++ b/PAGES_LEGALES.md @@ -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 +Mentions lĂ©gales +ConfidentialitĂ© +``` + +## 📊 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 diff --git a/POPUP_INFO_SUIVI.md b/POPUP_INFO_SUIVI.md new file mode 100644 index 0000000..ce22d85 --- /dev/null +++ b/POPUP_INFO_SUIVI.md @@ -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 + +``` + +## 🔒 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/) diff --git a/RESOLUTION_FINALE_TICKETS.md b/RESOLUTION_FINALE_TICKETS.md new file mode 100644 index 0000000..1093572 --- /dev/null +++ b/RESOLUTION_FINALE_TICKETS.md @@ -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* diff --git a/SUPPORT_INTERNAL_NOTIFICATIONS.md b/SUPPORT_INTERNAL_NOTIFICATIONS.md new file mode 100644 index 0000000..0127016 --- /dev/null +++ b/SUPPORT_INTERNAL_NOTIFICATIONS.md @@ -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 `
` +- 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 `
` +- 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 `
` 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 diff --git a/SUPPORT_INTERNAL_NOTIFICATIONS_SUMMARY.md b/SUPPORT_INTERNAL_NOTIFICATIONS_SUMMARY.md new file mode 100644 index 0000000..74213a3 --- /dev/null +++ b/SUPPORT_INTERNAL_NOTIFICATIONS_SUMMARY.md @@ -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 `
` + +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` → `
` 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) diff --git a/SUPPORT_TICKET_NOTIFICATIONS.md b/SUPPORT_TICKET_NOTIFICATIONS.md new file mode 100644 index 0000000..ad4fdd8 --- /dev/null +++ b/SUPPORT_TICKET_NOTIFICATIONS.md @@ -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 diff --git a/SUPPORT_TICKET_NOTIFICATIONS_FIXES.md b/SUPPORT_TICKET_NOTIFICATIONS_FIXES.md new file mode 100644 index 0000000..a6d85ac --- /dev/null +++ b/SUPPORT_TICKET_NOTIFICATIONS_FIXES.md @@ -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: +📋 [ticket page] Email: +📋 [ticket page] User metadata: +✅ [ticket page] Nom trouvĂ© dans display_name: +``` + +**Dans l'API recipient-info :** +``` +📋 [recipient-info] User metadata: +✅ [recipient-info] Nom trouvĂ© dans display_name: +``` + +--- + +## 🔍 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* diff --git a/TICKET_CREATOR_INFO_DISPLAY.md b/TICKET_CREATOR_INFO_DISPLAY.md new file mode 100644 index 0000000..1f33d01 --- /dev/null +++ b/TICKET_CREATOR_INFO_DISPLAY.md @@ -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 */} +
+ Ouvert par:{' '} + {creatorName} + {creatorRole && ( + + {formatRole(creatorRole)} + + )} +
+ +{/* Email si disponible */} +{creatorEmail && ( +
+ 📧 {creatorEmail} +
+)} +``` + +### ✅ 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* diff --git a/app/(app)/staff/tickets/[id]/page.tsx b/app/(app)/staff/tickets/[id]/page.tsx index 4b51426..7606bac 100644 --- a/app/(app)/staff/tickets/[id]/page.tsx +++ b/app/(app)/staff/tickets/[id]/page.tsx @@ -1,11 +1,32 @@ export const dynamic = "force-dynamic"; -import { createSbServer } from "@/lib/supabaseServer"; +import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer"; +import { Mail } from "lucide-react"; function formatDate(d?: string | null) { if (!d) return "—"; try { return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(d)); } catch { return d as string; } } +function formatRole(role: string): string { + const roleMap: Record = { + 'SUPER_ADMIN': 'Super Admin', + 'ADMIN': 'Administrateur', + 'AGENT': 'Agent', + 'USER': 'Utilisateur', + }; + return roleMap[role.toUpperCase()] || role; +} + +function getRoleBadgeColor(role: string): string { + const colorMap: Record = { + 'SUPER_ADMIN': 'bg-purple-100 text-purple-800', + 'ADMIN': 'bg-blue-100 text-blue-800', + 'AGENT': 'bg-green-100 text-green-800', + 'USER': 'bg-gray-100 text-gray-800', + }; + return colorMap[role.toUpperCase()] || 'bg-gray-100 text-gray-800'; +} + import StaffTicketActions from "@/components/staff/StaffTicketActions"; import MarkTicketRead from "@/components/staff/MarkTicketRead"; @@ -25,6 +46,80 @@ export default async function StaffTicketDetail({ params }: { params: { id: stri .maybeSingle(); if (tErr || !ticket) return (

Erreur

{tErr?.message || 'Ticket introuvable'}

); + console.log('📋 [ticket page] Ticket:', { id: ticket.id, created_by: ticket.created_by, org_id: ticket.org_id }); + + // RĂ©cupĂ©rer le nom de l'organisation + const { data: organization } = await sb + .from("organizations") + .select("name") + .eq("id", ticket.org_id) + .maybeSingle(); + + // RĂ©cupĂ©rer le nom de l'utilisateur crĂ©ateur + let creatorName = "Utilisateur inconnu"; + let creatorEmail = ""; + let creatorRole = ""; + + if (ticket.created_by) { + console.log('📋 [ticket page] Tentative getUserById pour:', ticket.created_by); + + // Utiliser le service role pour accĂ©der aux donnĂ©es auth + const sbAdmin = createSbServiceRole(); + const { data: creatorUser, error: userError } = await sbAdmin.auth.admin.getUserById(ticket.created_by); + console.log('📋 [ticket page] getUserById result:', { + hasData: !!creatorUser, + hasUser: !!creatorUser?.user, + hasEmail: !!creatorUser?.user?.email, + error: userError + }); + + if (creatorUser?.user) { + creatorEmail = creatorUser.user.email || ""; + + // RĂ©cupĂ©rer le nom depuis les mĂ©tadonnĂ©es utilisateur + const metadata = creatorUser.user.user_metadata; + + console.log('📋 [ticket page] User ID:', ticket.created_by); + console.log('📋 [ticket page] Email:', creatorUser.user.email); + console.log('📋 [ticket page] User metadata:', JSON.stringify(metadata, null, 2)); + + // PrioritĂ© 1: display_name (c'est lĂ  que Supabase Auth stocke le prĂ©nom) + if (metadata?.display_name) { + creatorName = metadata.display_name; + console.log('✅ [ticket page] Nom trouvĂ© dans display_name:', creatorName); + } + // PrioritĂ© 2: first_name + last_name + else if (metadata?.first_name) { + creatorName = metadata.last_name + ? `${metadata.first_name} ${metadata.last_name}` + : metadata.first_name; + console.log('✅ [ticket page] Nom trouvĂ© dans first_name:', creatorName); + } + // PrioritĂ© 3: email + else { + creatorName = creatorUser.user.email || "Utilisateur inconnu"; + console.log('⚠ [ticket page] Aucun nom trouvĂ©, utilisation de l\'email'); + } + + // RĂ©cupĂ©rer le rĂŽle depuis organization_members + const { data: orgMember } = await sb + .from("organization_members") + .select("role") + .eq("user_id", ticket.created_by) + .eq("org_id", ticket.org_id) + .maybeSingle(); + + if (orgMember?.role) { + creatorRole = orgMember.role; + console.log('✅ [ticket page] RĂŽle trouvĂ©:', creatorRole); + } + } else { + console.error('❌ [ticket page] Pas de donnĂ©es utilisateur retournĂ©es'); + } + } else { + console.log('⚠ [ticket page] Pas de created_by sur le ticket'); + } + const { data: messages } = await sb .from("ticket_messages") .select("id, ticket_id, author_id, body, internal, via, created_at") @@ -39,6 +134,31 @@ export default async function StaffTicketDetail({ params }: { params: { id: stri + {/* Afficher les informations de l'organisation et du crĂ©ateur */} +
+
+ Organisation:{' '} + {organization?.name || 'Non définie'} +
+
+
+ Ouvert par:{' '} + {creatorName} + {creatorRole && ( + + {formatRole(creatorRole)} + + )} +
+ {creatorEmail && ( +
+ + {creatorEmail} +
+ )} +
+
+
PrioritĂ©: {ticket.priority} ‱ Dernier message: {formatDate(ticket.last_message_at)} ‱ Non lus (client/staff): {ticket.unread_by_client || 0} / {ticket.unread_by_staff || 0}
diff --git a/app/api/tickets/[id]/messages/route.ts b/app/api/tickets/[id]/messages/route.ts index f27b5a1..9a3d7b1 100644 --- a/app/api/tickets/[id]/messages/route.ts +++ b/app/api/tickets/[id]/messages/route.ts @@ -1,11 +1,11 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer"; +import { sendSupportReplyEmail, sendInternalTicketReplyEmail } from "@/lib/emailMigrationHelpers"; export const dynamic = "force-dynamic"; export async function GET(_: Request, { params }: { params: { id: string } }) { - const sb = createRouteHandlerClient({ cookies }); + const sb = createSbServer(); // RĂ©cupĂ©rer les messages const { data: messages, error } = await sb @@ -40,7 +40,7 @@ export async function GET(_: Request, { params }: { params: { id: string } }) { } export async function POST(req: Request, { params }: { params: { id: string } }) { - const sb = createRouteHandlerClient({ cookies }); + const sb = createSbServer(); const { data: { user } } = await sb.auth.getUser(); if (!user) return new NextResponse("Unauthorized", { status: 401 }); @@ -70,6 +70,158 @@ export async function POST(req: Request, { params }: { params: { id: string } }) }); if (error) return NextResponse.json({ error: error.message }, { status: 400 }); + // Si c'est un message du staff et qu'il n'est pas interne, envoyer une notification par email + if (isStaff && !internal) { + try { + console.log('📧 [POST messages] Envoi de notification email...'); + + // RĂ©cupĂ©rer les informations du ticket + const { data: ticket } = await sb + .from("tickets") + .select("id, subject, created_by, org_id") + .eq("id", params.id) + .single(); + + console.log('📧 [POST messages] Ticket:', { id: ticket?.id, created_by: ticket?.created_by, org_id: ticket?.org_id }); + + if (ticket && ticket.created_by) { + // RĂ©cupĂ©rer les informations de l'organisation + const { data: organization } = await sb + .from("organizations") + .select("name, structure_api") + .eq("id", ticket.org_id) + .single(); + + console.log('📧 [POST messages] Organization:', { name: organization?.name, structure_api: organization?.structure_api }); + + // RĂ©cupĂ©rer le code employeur depuis organization_details + const { data: orgDetails } = await sb + .from("organization_details") + .select("code_employeur") + .eq("org_id", ticket.org_id) + .maybeSingle(); + + console.log('📧 [POST messages] Org details:', { code_employeur: orgDetails?.code_employeur }); + + // Utiliser le service role pour accĂ©der aux donnĂ©es auth + const sbAdmin = createSbServiceRole(); + + // RĂ©cupĂ©rer l'email et le prĂ©nom du crĂ©ateur du ticket + const { data: creatorUser, error: creatorError } = await sbAdmin.auth.admin.getUserById(ticket.created_by); + console.log('📧 [POST messages] Creator user:', { + hasData: !!creatorUser, + email: creatorUser?.user?.email, + error: creatorError + }); + + // RĂ©cupĂ©rer le nom du staff qui rĂ©pond + const { data: staffProfile, error: staffError } = await sbAdmin.auth.admin.getUserById(user.id); + console.log('📧 [POST messages] Staff profile:', { + hasData: !!staffProfile, + email: staffProfile?.user?.email, + error: staffError + }); + + if (creatorUser?.user?.email) { + // RĂ©cupĂ©rer le prĂ©nom depuis user_metadata + const firstName = creatorUser.user.user_metadata?.display_name || + creatorUser.user.user_metadata?.first_name || + undefined; + + // RĂ©cupĂ©rer le nom du staff depuis user_metadata + const staffName = staffProfile?.user?.user_metadata?.display_name || + staffProfile?.user?.user_metadata?.first_name || + staffProfile?.user?.email?.split('@')[0] || + 'L\'Ă©quipe support'; + + console.log('📧 [POST messages] Envoi vers:', creatorUser.user.email); + console.log('📧 [POST messages] DonnĂ©es email:', { + firstName, + staffName, + ticketId: ticket.id, + ticketSubject: ticket.subject, + organizationName: organization?.name, + employerCode: orgDetails?.code_employeur + }); + + // Utiliser le helper pour envoyer l'email + await sendSupportReplyEmail(creatorUser.user.email, { + firstName, + ticketId: ticket.id, + ticketSubject: ticket.subject, + staffName, + staffMessage: text, + organizationName: organization?.name, + employerCode: orgDetails?.code_employeur, + }); + + console.log(`✅ [POST messages] Notification email envoyĂ©e Ă  ${creatorUser.user.email} pour le ticket ${ticket.id}`); + } else { + console.error('❌ [POST messages] Email du crĂ©ateur introuvable'); + } + } else { + console.error('❌ [POST messages] Ticket ou created_by introuvable'); + } + } catch (emailError) { + // On ne fait pas Ă©chouer la requĂȘte si l'email Ă©choue + console.error('❌ [POST messages] Erreur lors de l\'envoi de la notification email:', emailError); + } + } + + // Si c'est un message d'un utilisateur (non-staff), envoyer une notification interne + if (!isStaff) { + try { + console.log('📧 [POST messages] Envoi de notification interne...'); + + // RĂ©cupĂ©rer les informations du ticket + const { data: ticket } = await sb + .from("tickets") + .select("id, subject, status, org_id") + .eq("id", params.id) + .single(); + + if (ticket) { + // RĂ©cupĂ©rer les informations de l'organisation + const { data: organization } = await sb + .from("organizations") + .select("name") + .eq("id", ticket.org_id) + .single(); + + // RĂ©cupĂ©rer le code employeur depuis organization_details + const { data: orgDetails } = await sb + .from("organization_details") + .select("code_employeur") + .eq("org_id", ticket.org_id) + .maybeSingle(); + + // Utiliser le service role pour accĂ©der aux donnĂ©es auth + const sbAdmin = createSbServiceRole(); + + // RĂ©cupĂ©rer les infos de l'utilisateur qui rĂ©pond + const { data: userData } = await sbAdmin.auth.admin.getUserById(user.id); + const userName = userData?.user?.user_metadata?.display_name + || userData?.user?.user_metadata?.first_name + || 'Utilisateur inconnu'; + + await sendInternalTicketReplyEmail({ + ticketId: ticket.id, + ticketSubject: ticket.subject, + ticketStatus: ticket.status, + userMessage: text, + userName, + userEmail: user.email || 'Email non disponible', + organizationName: organization?.name, + employerCode: orgDetails?.code_employeur, + }); + + console.log(`✅ [POST messages] Notification interne envoyĂ©e pour le ticket ${ticket.id}`); + } + } catch (emailError) { + console.error('❌ [POST messages] Erreur lors de l\'envoi de la notification interne:', emailError); + } + } + return NextResponse.json({ ok: true }, { status: 201 }); } diff --git a/app/api/tickets/[id]/recipient-info/route.ts b/app/api/tickets/[id]/recipient-info/route.ts new file mode 100644 index 0000000..e002ae3 --- /dev/null +++ b/app/api/tickets/[id]/recipient-info/route.ts @@ -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 + }); +} diff --git a/app/api/tickets/route.ts b/app/api/tickets/route.ts index d2adcbe..42f64d4 100644 --- a/app/api/tickets/route.ts +++ b/app/api/tickets/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { sendInternalTicketCreatedEmail } from "@/lib/emailMigrationHelpers"; +import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer"; export const dynamic = "force-dynamic"; @@ -94,6 +96,53 @@ export async function POST(req: Request) { .update({ message_count: 1 }) .eq("id", ticket.id); + // Envoyer une notification interne par email Ă  paie@odentas.fr si le ticket n'est pas créé par le staff + if (!isStaff) { + try { + console.log('[TICKET CREATE] Sending internal notification email...'); + + // RĂ©cupĂ©rer les infos de l'utilisateur + const sbAdmin = createSbServiceRole(); + const { data: userData } = await sbAdmin.auth.admin.getUserById(user.id); + const userName = userData?.user?.user_metadata?.display_name + || userData?.user?.user_metadata?.first_name + || 'Utilisateur inconnu'; + + // RĂ©cupĂ©rer les infos de l'organisation + const { data: organization } = await sb + .from('organizations') + .select('name') + .eq('id', org_id) + .single(); + + // RĂ©cupĂ©rer le code employeur + const { data: orgDetails } = await sb + .from('organization_details') + .select('code_employeur') + .eq('org_id', org_id) + .single(); + + // DĂ©terminer la catĂ©gorie du ticket (basĂ©e sur le sujet pour l'instant) + const category = 'Support gĂ©nĂ©ral'; + + await sendInternalTicketCreatedEmail({ + ticketId: ticket.id, + ticketSubject: subject, + ticketCategory: category, + ticketMessage: message, + userName, + userEmail: user.email || 'Email non disponible', + organizationName: organization?.name, + employerCode: orgDetails?.code_employeur, + }); + + console.log('[TICKET CREATE] Internal notification email sent successfully'); + } catch (emailError) { + // Ne pas bloquer la crĂ©ation du ticket si l'email Ă©choue + console.error('[TICKET CREATE] Failed to send internal notification email:', emailError); + } + } + return NextResponse.json(ticket, { status: 201 }); } diff --git a/app/layout.tsx b/app/layout.tsx index 471598c..941e31b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import Providers from "@/components/Providers"; import ProgressBar from "@/components/ProgressBar"; import { PostHogPageView } from "@/components/PostHogPageView"; import PostHogIdentifier from "@/components/PostHogIdentifier"; +import PopupInfoSuivi from "@/components/PopupInfoSuivi"; import { useEffect, Suspense } from "react"; /** @@ -72,6 +73,8 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} + {/* Popup d'information sur la confidentialitĂ© et le suivi */} + {/* BugReporter temporairement masquĂ© */} diff --git a/app/mentions-legales/MentionsLegalesContent.tsx b/app/mentions-legales/MentionsLegalesContent.tsx new file mode 100644 index 0000000..c9986fe --- /dev/null +++ b/app/mentions-legales/MentionsLegalesContent.tsx @@ -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 ( +
+ + ); +} diff --git a/app/mentions-legales/page.tsx b/app/mentions-legales/page.tsx new file mode 100644 index 0000000..1278a14 --- /dev/null +++ b/app/mentions-legales/page.tsx @@ -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 ; +} diff --git a/app/politique-confidentialite/PolitiqueConfidentialiteContent.tsx b/app/politique-confidentialite/PolitiqueConfidentialiteContent.tsx new file mode 100644 index 0000000..5e586db --- /dev/null +++ b/app/politique-confidentialite/PolitiqueConfidentialiteContent.tsx @@ -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 ( +
+ + ); +} diff --git a/app/politique-confidentialite/page.tsx b/app/politique-confidentialite/page.tsx new file mode 100644 index 0000000..cf3349a --- /dev/null +++ b/app/politique-confidentialite/page.tsx @@ -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 ; +} diff --git a/components/PopupInfoSuivi.tsx b/components/PopupInfoSuivi.tsx new file mode 100644 index 0000000..394da6d --- /dev/null +++ b/components/PopupInfoSuivi.tsx @@ -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 ( + + {open && ( + +
+ + +
+
+ +
+
+

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Ă© + + . +

+ +
+ + + En savoir plus + +
+
+
+
+
+ )} +
+ ); +} diff --git a/components/staff/StaffTicketActions.tsx b/components/staff/StaffTicketActions.tsx index 1e5c6d3..74bf874 100644 --- a/components/staff/StaffTicketActions.tsx +++ b/components/staff/StaffTicketActions.tsx @@ -1,9 +1,39 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { TicketReplyConfirmationModal } from "./TicketReplyConfirmationModal"; export default function StaffTicketActions({ ticketId, status, mode = "both" }: { ticketId: string; status: string; mode?: "status" | "message" | "both" }) { const [updating, setUpdating] = useState(false); const [sending, setSending] = useState(false); + const [showModal, setShowModal] = useState(false); + const [pendingMessage, setPendingMessage] = useState({ body: "", internal: false }); + const [recipientInfo, setRecipientInfo] = useState<{ email: string; name: string } | null>(null); + + // RĂ©cupĂ©rer les informations du destinataire + useEffect(() => { + async function fetchRecipientInfo() { + try { + console.log('🔍 [StaffTicketActions] Fetching recipient info for ticket:', ticketId); + const url = `/api/tickets/${ticketId}/recipient-info`; + console.log('🔍 [StaffTicketActions] URL:', url); + + const res = await fetch(url, { credentials: 'include' }); + console.log('🔍 [StaffTicketActions] Response status:', res.status); + + if (res.ok) { + const data = await res.json(); + console.log('✅ [StaffTicketActions] Recipient info:', data); + setRecipientInfo(data); + } else { + const errorText = await res.text(); + console.error('❌ [StaffTicketActions] Error response:', res.status, errorText); + } + } catch (e) { + console.error('❌ [StaffTicketActions] Erreur lors de la rĂ©cupĂ©ration des infos destinataire:', e); + } + } + fetchRecipientInfo(); + }, [ticketId]); async function onStatusSubmit(e: React.FormEvent) { e.preventDefault(); @@ -27,20 +57,46 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }: const fd = new FormData(form); const body = String(fd.get('body') || '').trim(); const internal = fd.get('internal') === 'on'; + if (!body) return; + + // Si c'est une note interne, envoyer directement + if (internal) { + await sendMessage(body, internal, form); + } else { + // Sinon, afficher le modal de confirmation + setPendingMessage({ body, internal }); + setShowModal(true); + } + } + + async function sendMessage(body: string, internal: boolean, formToReset?: HTMLFormElement) { setSending(true); try { - const res = await fetch(`/api/tickets/${ticketId}/messages`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body, internal }) }); + const res = await fetch(`/api/tickets/${ticketId}/messages`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ body, internal }) + }); if (!res.ok) throw new Error(await res.text()); - form.reset(); + + if (formToReset) formToReset.reset(); location.reload(); } catch (e: any) { alert('Erreur: ' + (e?.message || 'inconnue')); } finally { setSending(false); + setShowModal(false); } } + function handleConfirmSend() { + // RĂ©cupĂ©rer le formulaire pour le reset + const form = document.querySelector('form[data-ticket-form]') as HTMLFormElement; + sendMessage(pendingMessage.body, pendingMessage.internal, form); + } + return ( <> {(mode === 'status' || mode === 'both') && ( @@ -56,7 +112,7 @@ export default function StaffTicketActions({ ticketId, status, mode = "both" }: )} {(mode === 'message' || mode === 'both') && ( -
+