ID PostHog + affichage prod et salariés nouveau CDDU + problème DPAE contrats
This commit is contained in:
parent
2f7c5dad7e
commit
61ae5b0bb4
5 changed files with 142 additions and 35 deletions
|
|
@ -18,15 +18,32 @@ PostHog est configuré pour tracker automatiquement les événements utilisateur
|
|||
- Initialise PostHog au chargement de l'application
|
||||
- Active le mode debug en développement
|
||||
- Expose `window.posthog` pour les tests console en dev
|
||||
- **Important** : Utilise `person_profiles: 'identified_only'` - les utilisateurs ne sont créés dans PostHog que lorsqu'ils sont explicitement identifiés
|
||||
|
||||
2. **PostHogPageView** (`components/PostHogPageView.tsx`)
|
||||
- Track automatiquement chaque changement de page
|
||||
- Capture l'URL complète avec query params
|
||||
|
||||
3. **PostHogIdentifier** (`components/PostHogIdentifier.tsx`)
|
||||
- Identifie automatiquement les utilisateurs connectés
|
||||
- **Composant clé pour l'identification des utilisateurs** ⭐
|
||||
- Identifie automatiquement les utilisateurs connectés via l'API `/api/me`
|
||||
- Associe les propriétés utilisateur (email, nom, company, etc.)
|
||||
- Réinitialise l'identité lors de la déconnexion
|
||||
- **Note** : Si vous ne voyez pas les utilisateurs identifiés dans PostHog, vérifiez que ce composant est bien présent dans votre layout
|
||||
|
||||
## 🔍 Pourquoi les utilisateurs sont anonymes ?
|
||||
|
||||
Si vous voyez tous les utilisateurs anonymisés dans PostHog, c'est probablement parce que :
|
||||
|
||||
1. ❌ **Le composant `PostHogIdentifier` n'est pas présent** dans le layout
|
||||
2. ❌ **L'API `/api/me` ne retourne pas les bonnes données** (vérifiez la structure JSON)
|
||||
3. ❌ **L'utilisateur n'est pas authentifié** au moment où PostHog charge
|
||||
4. ❌ **Les variables d'environnement PostHog sont manquantes**
|
||||
|
||||
### Solution :
|
||||
- Vérifiez que `<PostHogIdentifier />` est bien dans `app/layout.tsx`
|
||||
- Ouvrez la console du navigateur et regardez les logs : vous devriez voir `👤 PostHog: Utilisateur identifié: email@example.com`
|
||||
- Si vous voyez une erreur, vérifiez que l'API `/api/me` fonctionne correctement
|
||||
|
||||
## 📈 Événements trackés automatiquement
|
||||
|
||||
|
|
@ -39,9 +56,9 @@ PostHog est configuré pour tracker automatiquement les événements utilisateur
|
|||
Lors de l'identification (connexion) :
|
||||
- `email` : Email de l'utilisateur
|
||||
- `first_name` : Prénom
|
||||
- `last_name` : Nom
|
||||
- `company_name` : Nom de la société
|
||||
- `company_id` : ID de la société
|
||||
- `display_name` : Nom d'affichage complet
|
||||
- `company_name` : Nom de la société active (active_org_name)
|
||||
- `company_id` : ID de la société active (active_org_id)
|
||||
- `is_staff` : Membre du staff ou non
|
||||
|
||||
## 🔧 Utilisation avancée
|
||||
|
|
@ -227,6 +244,42 @@ posthog.opt_in_capturing();
|
|||
4. ✅ Vérifier Network : requêtes `/batch/` en status 200
|
||||
5. ✅ Vérifier la clé API dans le dashboard PostHog
|
||||
|
||||
### ❌ Les utilisateurs sont tous anonymes / non identifiés
|
||||
|
||||
**Symptôme** : Dans PostHog, vous voyez des utilisateurs avec des IDs anonymes (ex: `anon_xyz123`) au lieu des vrais utilisateurs.
|
||||
|
||||
**Cause** : Le composant `PostHogIdentifier` n'identifie pas correctement les utilisateurs.
|
||||
|
||||
**Solutions** :
|
||||
|
||||
1. **Vérifier la console du navigateur** :
|
||||
- Vous devriez voir : `👤 PostHog: Utilisateur identifié: email@example.com ID: user-uuid`
|
||||
- Si vous voyez une erreur, notez le message
|
||||
|
||||
2. **Vérifier que PostHogIdentifier est dans le layout** :
|
||||
- Ouvrir `app/layout.tsx`
|
||||
- Chercher `<PostHogIdentifier />`
|
||||
- Doit être à l'intérieur du `<Providers>` ou `<PostHogProvider>`
|
||||
|
||||
3. **Tester l'API /api/me** :
|
||||
```bash
|
||||
# Dans la console du navigateur (quand vous êtes connecté)
|
||||
fetch('/api/me').then(r => r.json()).then(console.log)
|
||||
```
|
||||
- Doit retourner : `{ user: { id: "...", email: "..." }, active_org_id: "...", ... }`
|
||||
- Si `user.id` est undefined, il y a un problème avec l'API
|
||||
|
||||
4. **Vérifier la configuration PostHog** :
|
||||
- Dans `PostHogProvider.tsx`, vous devez avoir : `person_profiles: 'identified_only'`
|
||||
- Cela signifie que PostHog ne crée des profils que pour les utilisateurs identifiés explicitement
|
||||
|
||||
5. **Forcer une réidentification** :
|
||||
```javascript
|
||||
// Dans la console du navigateur
|
||||
window.posthog.reset(); // Efface l'identité actuelle
|
||||
window.location.reload(); // Recharge la page pour réidentifier
|
||||
```
|
||||
|
||||
### Double initialisation
|
||||
|
||||
Si vous voyez "You have already initialized PostHog!", c'est normal en dev (React Strict Mode). En production, cela n'arrive pas.
|
||||
|
|
|
|||
|
|
@ -594,7 +594,9 @@ export default function ContratMultiPage() {
|
|||
{(() => {
|
||||
const raw = String(data.dpae || "");
|
||||
const norm = raw.normalize("NFD").replace(/\p{Diacritic}/gu, "").trim().toLowerCase();
|
||||
if (norm === "ok" || norm === "envoyee" || norm === "retourok" || norm === "retour_ok") {
|
||||
|
||||
// États "OK" / "Effectuée"
|
||||
if (norm === "ok" || norm === "faite" || norm === "envoyee" || norm === "retourok" || norm === "retour_ok") {
|
||||
return (
|
||||
<Field
|
||||
label="DPAE"
|
||||
|
|
@ -606,7 +608,9 @@ export default function ContratMultiPage() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (norm === "a traiter" || norm === "a_traiter" || norm === "a_traiter") {
|
||||
|
||||
// États "À traiter" / "En cours"
|
||||
if (norm === "a traiter" || norm === "a_traiter" || norm === "a faire" || norm === "a_faire") {
|
||||
return (
|
||||
<Field
|
||||
label="DPAE"
|
||||
|
|
@ -618,6 +622,27 @@ export default function ContratMultiPage() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// État "Refusée"
|
||||
if (norm === "refusee" || norm === "refuse") {
|
||||
return (
|
||||
<Field
|
||||
label="DPAE"
|
||||
value={(
|
||||
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-rose-100 text-rose-800">
|
||||
<Clock className="w-3 h-3" /> Refusée
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Si une valeur existe mais n'est pas reconnue, l'afficher telle quelle
|
||||
if (raw) {
|
||||
return <Field label="DPAE" value={<span className="text-xs text-slate-600">{raw}</span>} />;
|
||||
}
|
||||
|
||||
// Aucune valeur
|
||||
return <Field label="DPAE" value={<span className="text-xs text-slate-400">—</span>} />;
|
||||
})()}
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -1344,19 +1344,45 @@ return (
|
|||
<Section title="Déclarations" icon={Shield}>
|
||||
<Field
|
||||
label="DPAE"
|
||||
value={
|
||||
data.dpae === "OK" ? (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800">
|
||||
<CheckCircle className="w-3 h-3" /> Effectuée
|
||||
</span>
|
||||
) : data.dpae === "À traiter" ? (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
|
||||
<Clock className="w-3 h-3" /> En cours
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-slate-400">—</span>
|
||||
)
|
||||
}
|
||||
value={(() => {
|
||||
const raw = String(data.dpae || "");
|
||||
const norm = raw.normalize("NFD").replace(/\p{Diacritic}/gu, "").trim().toLowerCase();
|
||||
|
||||
// États "OK" / "Effectuée"
|
||||
if (norm === "ok" || norm === "faite" || norm === "envoyee" || norm === "retourok" || norm === "retour_ok") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800">
|
||||
<CheckCircle className="w-3 h-3" /> Effectuée
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// États "À traiter" / "En cours"
|
||||
if (norm === "a traiter" || norm === "a_traiter" || norm === "a faire" || norm === "a_faire") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
|
||||
<Clock className="w-3 h-3" /> En cours
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// État "Refusée"
|
||||
if (norm === "refusee" || norm === "refuse") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-rose-100 text-rose-800">
|
||||
<Clock className="w-3 h-3" /> Refusée
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Si une valeur existe mais n'est pas reconnue, l'afficher telle quelle
|
||||
if (raw) {
|
||||
return <span className="text-xs text-slate-600">{raw}</span>;
|
||||
}
|
||||
|
||||
// Aucune valeur
|
||||
return <span className="text-xs text-slate-400">—</span>;
|
||||
})()}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,19 +22,22 @@ export default function PostHogIdentifier() {
|
|||
|
||||
const me = await res.json();
|
||||
|
||||
if (me?.id) {
|
||||
// L'API /api/me retourne { user: { id, email, ... }, active_org_id, ... }
|
||||
const userId = me?.user?.id || me?.user_id;
|
||||
|
||||
if (userId) {
|
||||
// Identifier l'utilisateur avec son ID unique
|
||||
posthog?.identify(me.id, {
|
||||
email: me.email,
|
||||
first_name: me.first_name,
|
||||
last_name: me.last_name,
|
||||
company_name: me.company_name,
|
||||
company_id: me.company_id,
|
||||
posthog?.identify(userId, {
|
||||
email: me.user?.email || me.email,
|
||||
first_name: me.user?.first_name || me.first_name,
|
||||
display_name: me.user?.display_name || me.display_name,
|
||||
company_name: me.active_org_name,
|
||||
company_id: me.active_org_id,
|
||||
is_staff: me.is_staff || false,
|
||||
// Ajoutez d'autres propriétés utiles pour vos analytics
|
||||
});
|
||||
|
||||
console.log('👤 PostHog: Utilisateur identifié:', me.email);
|
||||
console.log('👤 PostHog: Utilisateur identifié:', me.user?.email || me.email, 'ID:', userId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ PostHog: Erreur lors de l\'identification:', e);
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ function useSearchSalaries(q: string, orgId?: string | null) {
|
|||
api<{ items: SalarieOption[] }>(`/salaries?${params.toString()}`, {}, clientInfo).then((r) => ({
|
||||
items: r.items?.map((s) => ({ matricule: s.matricule, nom: s.nom, email: s.email ?? undefined })) ?? [],
|
||||
})),
|
||||
enabled: q.trim().length > 1 && (!!orgId || !!clientInfo),
|
||||
enabled: (!!orgId || !!clientInfo),
|
||||
staleTime: 10_000,
|
||||
});
|
||||
}
|
||||
|
|
@ -266,7 +266,7 @@ function useSearchSpectacles(q: string, orgId?: string | null) {
|
|||
numero_objet: (s as any).numero_objet || (s as any)["n° d'objet"] || (s as any).numero || null,
|
||||
})) ?? [],
|
||||
})),
|
||||
enabled: q.trim().length > 1 && !!clientInfo,
|
||||
enabled: !!clientInfo,
|
||||
staleTime: 10_000,
|
||||
});
|
||||
}
|
||||
|
|
@ -1332,7 +1332,7 @@ useEffect(() => {
|
|||
onChange={(e) => setSpectacleQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
const items = spectacleSearch?.items || [];
|
||||
if (spectacleQuery.length <= 1 || items.length === 0) return;
|
||||
if (items.length === 0) return;
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setSpectacleActive(i => Math.min(i + 1, items.length - 1)); }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setSpectacleActive(i => Math.max(i - 1, 0)); }
|
||||
if (e.key === 'Enter') {
|
||||
|
|
@ -1349,7 +1349,7 @@ useEffect(() => {
|
|||
(e.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
placeholder="Rechercher une production (min. 2 lettres)…"
|
||||
placeholder="Rechercher une production…"
|
||||
className="w-full pl-9 px-3 py-2 rounded-lg border bg-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1376,7 +1376,7 @@ useEffect(() => {
|
|||
Recherche…
|
||||
</div>
|
||||
)}
|
||||
{spectacleSearch?.items && spectacleQuery.length > 1 && (
|
||||
{spectacleSearch?.items && (
|
||||
<div className="mt-2 max-h-56 overflow-auto rounded-lg border">
|
||||
{spectacleSearch.items.length === 0 ? (
|
||||
<div className="p-3 text-sm text-slate-500">Aucun résultat.</div>
|
||||
|
|
@ -1451,7 +1451,7 @@ useEffect(() => {
|
|||
onChange={(e) => setSalarieQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
const items = salarieSearch?.items || [];
|
||||
if (salarieQuery.length <= 1 || items.length === 0) return;
|
||||
if (items.length === 0) return;
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setSalarieActive(i => Math.min(i + 1, items.length - 1)); }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setSalarieActive(i => Math.max(i - 1, 0)); }
|
||||
if (e.key === 'Enter') {
|
||||
|
|
@ -1464,7 +1464,7 @@ useEffect(() => {
|
|||
(e.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
placeholder="Rechercher un salarié (min. 2 lettres)…"
|
||||
placeholder="Rechercher un salarié…"
|
||||
className="w-full pl-9 px-3 py-2 rounded-lg border bg-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1483,7 +1483,7 @@ useEffect(() => {
|
|||
Recherche…
|
||||
</div>
|
||||
)}
|
||||
{salarieSearch?.items && salarieQuery.length > 1 && (
|
||||
{salarieSearch?.items && (
|
||||
<div className="mt-2 max-h-56 overflow-auto rounded-lg border">
|
||||
{salarieSearch.items.length === 0 ? (
|
||||
<div className="p-3 text-sm text-slate-500">Aucun résultat.</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue