Modifs 13/10 soir
This commit is contained in:
parent
e29c648041
commit
9d54549493
19 changed files with 581 additions and 71 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -18,4 +18,5 @@ build/
|
|||
*.log
|
||||
|
||||
# Tests & coverage
|
||||
coverage/
|
||||
coverage/
|
||||
.vercel
|
||||
|
|
|
|||
247
POSTHOG_ANALYTICS_GUIDE.md
Normal file
247
POSTHOG_ANALYTICS_GUIDE.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# 📊 PostHog Analytics - Guide d'Utilisation
|
||||
|
||||
## 🎯 Vue d'ensemble
|
||||
|
||||
PostHog est configuré pour tracker automatiquement les événements utilisateurs dans l'application Odentas Espace Paie.
|
||||
|
||||
## ✅ Configuration actuelle
|
||||
|
||||
### Installation
|
||||
- **Package** : `posthog-js` (client) + `posthog-node` (serveur)
|
||||
- **Région** : EU (https://eu.i.posthog.com)
|
||||
- **Variables d'environnement** :
|
||||
- `NEXT_PUBLIC_POSTHOG_KEY` : Clé API publique
|
||||
- `NEXT_PUBLIC_POSTHOG_HOST` : URL du serveur PostHog
|
||||
|
||||
### Composants créés
|
||||
1. **PostHogProvider** (`components/PostHogProvider.tsx`)
|
||||
- Initialise PostHog au chargement de l'application
|
||||
- Active le mode debug en développement
|
||||
- Expose `window.posthog` pour les tests console en dev
|
||||
|
||||
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
|
||||
- Associe les propriétés utilisateur (email, nom, company, etc.)
|
||||
- Réinitialise l'identité lors de la déconnexion
|
||||
|
||||
## 📈 Événements trackés automatiquement
|
||||
|
||||
### Événements système
|
||||
- `$pageview` : Chaque changement de page
|
||||
- `$pageleave` : Quand l'utilisateur quitte une page
|
||||
- `$autocapture` : Clics et interactions (si activé)
|
||||
|
||||
### Propriétés utilisateur trackées
|
||||
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é
|
||||
- `is_staff` : Membre du staff ou non
|
||||
|
||||
## 🔧 Utilisation avancée
|
||||
|
||||
### Tracker un événement personnalisé
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { usePostHog } from 'posthog-js/react';
|
||||
|
||||
function MonComposant() {
|
||||
const posthog = usePostHog();
|
||||
|
||||
const handleAction = () => {
|
||||
posthog.capture('nom_evenement', {
|
||||
propriete1: 'valeur',
|
||||
propriete2: 123,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
return <button onClick={handleAction}>Action</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Identifier un utilisateur manuellement
|
||||
|
||||
```typescript
|
||||
posthog.identify('user_id_unique', {
|
||||
email: 'user@example.com',
|
||||
name: 'Nom Utilisateur',
|
||||
plan: 'premium'
|
||||
});
|
||||
```
|
||||
|
||||
### Tracker une propriété super (persistante)
|
||||
|
||||
```typescript
|
||||
// Sera ajoutée à tous les événements futurs
|
||||
posthog.register({
|
||||
app_version: '1.0.0',
|
||||
environment: 'production'
|
||||
});
|
||||
```
|
||||
|
||||
### Réinitialiser l'identité
|
||||
|
||||
```typescript
|
||||
// À la déconnexion (déjà fait dans LogoutButton)
|
||||
posthog.reset();
|
||||
```
|
||||
|
||||
## 🧪 Tests en développement
|
||||
|
||||
### Console du navigateur
|
||||
|
||||
En mode développement, PostHog est accessible via `window.posthog` :
|
||||
|
||||
```javascript
|
||||
// Tester un événement
|
||||
window.posthog.capture('test_event', { message: 'Hello' });
|
||||
|
||||
// Voir l'utilisateur actuel
|
||||
window.posthog.get_distinct_id();
|
||||
|
||||
// Voir les propriétés
|
||||
window.posthog.get_property('email');
|
||||
```
|
||||
|
||||
### Vérifier les requêtes réseau
|
||||
|
||||
1. Ouvrez DevTools (F12)
|
||||
2. Onglet Network
|
||||
3. Filtrez par "batch" ou "posthog"
|
||||
4. Les requêtes vers `eu.i.posthog.com/batch/` doivent être en status 200
|
||||
|
||||
### Logs console
|
||||
|
||||
Avec le mode debug activé, vous verrez :
|
||||
```
|
||||
[PostHog.js] "capture" {event: "$pageview", properties: {...}}
|
||||
[PostHog.js] "identify" {distinctId: "user_id", properties: {...}}
|
||||
```
|
||||
|
||||
## 📊 Dashboard PostHog
|
||||
|
||||
### Accès
|
||||
https://eu.posthog.com/
|
||||
|
||||
### Vues utiles
|
||||
|
||||
1. **Activity / Live Events** : Voir les événements en temps réel
|
||||
2. **Persons** : Liste des utilisateurs identifiés
|
||||
3. **Insights** : Créer des graphiques et analyses
|
||||
4. **Funnels** : Analyser les parcours utilisateurs
|
||||
5. **Session Recordings** : Voir les replays de sessions (si activé)
|
||||
|
||||
### Filtres utiles
|
||||
|
||||
- Exclure dev : `$current_url does not contain localhost`
|
||||
- Voir uniquement staff : `is_staff = true`
|
||||
- Par société : `company_name = X`
|
||||
|
||||
## 🎯 Cas d'usage recommandés
|
||||
|
||||
### 1. Tracker les actions importantes
|
||||
|
||||
```typescript
|
||||
// Création d'un contrat
|
||||
posthog.capture('contrat_created', {
|
||||
contrat_id: contratId,
|
||||
type: typeContrat,
|
||||
montant: montant
|
||||
});
|
||||
|
||||
// Signature d'un document
|
||||
posthog.capture('document_signed', {
|
||||
document_type: 'contrat',
|
||||
document_id: docId
|
||||
});
|
||||
|
||||
// Téléchargement de fiche de paie
|
||||
posthog.capture('payslip_downloaded', {
|
||||
month: '2025-10',
|
||||
employee_id: employeeId
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Groupes (pour analytics par société)
|
||||
|
||||
```typescript
|
||||
posthog.group('company', companyId, {
|
||||
company_name: 'Société X',
|
||||
plan: 'premium',
|
||||
employees_count: 50
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Feature Flags (A/B testing)
|
||||
|
||||
```typescript
|
||||
const showNewFeature = posthog.isFeatureEnabled('new_ui_design');
|
||||
|
||||
if (showNewFeature) {
|
||||
// Afficher la nouvelle interface
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 Confidentialité & RGPD
|
||||
|
||||
### Données collectées
|
||||
- Événements de navigation et interactions
|
||||
- Propriétés utilisateur (email, nom, société)
|
||||
- Données techniques (navigateur, OS, résolution)
|
||||
|
||||
### Données NON collectées par défaut
|
||||
- Mots de passe
|
||||
- Données sensibles dans les formulaires
|
||||
- Contenu des documents
|
||||
|
||||
### Respect du RGPD
|
||||
- Hébergement EU (GDPR compliant)
|
||||
- Possibilité d'anonymiser les IPs
|
||||
- Droit à l'oubli via l'API PostHog
|
||||
|
||||
### Opt-out utilisateur
|
||||
|
||||
```typescript
|
||||
// Si l'utilisateur refuse le tracking
|
||||
posthog.opt_out_capturing();
|
||||
|
||||
// Pour réactiver
|
||||
posthog.opt_in_capturing();
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Les événements n'arrivent pas
|
||||
|
||||
1. ✅ Vérifier que les variables `NEXT_PUBLIC_*` sont dans `.env.local`
|
||||
2. ✅ Redémarrer le serveur après modification du `.env.local`
|
||||
3. ✅ Vérifier la console : doit afficher "✅ PostHog initialisé"
|
||||
4. ✅ Vérifier Network : requêtes `/batch/` en status 200
|
||||
5. ✅ Vérifier la clé API dans le dashboard PostHog
|
||||
|
||||
### Double initialisation
|
||||
|
||||
Si vous voyez "You have already initialized PostHog!", c'est normal en dev (React Strict Mode). En production, cela n'arrive pas.
|
||||
|
||||
### Événements en retard
|
||||
|
||||
PostHog batch les événements pour optimiser les performances. Ils peuvent apparaître avec 5-30s de délai dans le dashboard.
|
||||
|
||||
## 📚 Ressources
|
||||
|
||||
- [Documentation PostHog](https://posthog.com/docs)
|
||||
- [API JavaScript](https://posthog.com/docs/libraries/js)
|
||||
- [React Integration](https://posthog.com/docs/libraries/react)
|
||||
- [Feature Flags](https://posthog.com/docs/feature-flags)
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 13 octobre 2025
|
||||
|
|
@ -103,13 +103,13 @@ function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "term
|
|||
|
||||
// --- Mapping état → couleur/texte
|
||||
const ETATS: Record<Contrat["etat"], { label: string; className: string }> = {
|
||||
"pre-demande": { label: "Pré-demande", className: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300" },
|
||||
"Reçue": { label: "Reçue", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300" },
|
||||
"envoye": { label: "Envoyé", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300" },
|
||||
"signe": { label: "Contrat signé", className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300" },
|
||||
"modification": { label: "Modifier la demande", className: "bg-rose-100 text-rose-800 dark:bg-rose-900/40 dark:text-rose-300" },
|
||||
"traitee": { label: "Traitée", className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300" },
|
||||
"en_cours": { label: "En cours", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300" },
|
||||
"pre-demande": { label: "Pré-demande", className: "bg-slate-100 text-slate-700" },
|
||||
"Reçue": { label: "Reçue", className: "bg-blue-100 text-blue-800" },
|
||||
"envoye": { label: "Envoyé", className: "bg-blue-100 text-blue-800" },
|
||||
"signe": { label: "Contrat signé", className: "bg-emerald-100 text-emerald-800" },
|
||||
"modification": { label: "Modifier la demande", className: "bg-rose-100 text-rose-800" },
|
||||
"traitee": { label: "Traitée", className: "bg-emerald-100 text-emerald-800" },
|
||||
"en_cours": { label: "En cours", className: "bg-blue-100 text-blue-800" },
|
||||
};
|
||||
|
||||
function humanizeEtat(raw?: string){
|
||||
|
|
@ -140,13 +140,13 @@ type PaginationProps = {
|
|||
|
||||
function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChange, isFetching, itemsCount, position = 'bottom' }: PaginationProps) {
|
||||
return (
|
||||
<div className={`p-3 flex flex-col sm:flex-row items-center gap-3 ${position === 'bottom' ? 'border-t dark:border-slate-800' : ''}`}>
|
||||
<div className={`p-3 flex flex-col sm:flex-row items-center gap-3 ${position === 'bottom' ? 'border-t' : ''}`}>
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onPageChange(Math.max(1, page - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-2 py-1 rounded-lg border dark:border-slate-800 disabled:opacity-40 hover:bg-slate-50 dark:hover:bg-slate-800 disabled:hover:bg-transparent"
|
||||
className="px-2 py-1 rounded-lg border disabled:opacity-40 hover:bg-slate-50 disabled:hover:bg-transparent"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4"/>
|
||||
</button>
|
||||
|
|
@ -156,7 +156,7 @@ function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChang
|
|||
<button
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className="px-2 py-1 rounded-lg border dark:border-slate-800 disabled:opacity-40 hover:bg-slate-50 dark:hover:bg-slate-800 disabled:hover:bg-transparent"
|
||||
className="px-2 py-1 rounded-lg border disabled:opacity-40 hover:bg-slate-50 disabled:hover:bg-transparent"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4"/>
|
||||
</button>
|
||||
|
|
@ -164,11 +164,11 @@ function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChang
|
|||
|
||||
{/* Sélecteur de limite */}
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-slate-600 dark:text-slate-400">Afficher :</span>
|
||||
<span className="text-slate-600">Afficher :</span>
|
||||
<select
|
||||
value={limit}
|
||||
onChange={(e) => onLimitChange(parseInt(e.target.value, 10))}
|
||||
className="px-2 py-1 rounded-lg border dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||||
className="px-2 py-1 rounded-lg border bg-white text-sm"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={50}>50</option>
|
||||
|
|
@ -177,7 +177,7 @@ function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChang
|
|||
</div>
|
||||
|
||||
{/* Informations */}
|
||||
<div className="sm:ml-auto text-sm text-slate-600 dark:text-slate-400">
|
||||
<div className="sm:ml-auto text-sm text-slate-600">
|
||||
{isFetching ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
|
|
@ -206,7 +206,7 @@ function safeEtat(etat?: string){
|
|||
const label = etat ? etat.charAt(0).toUpperCase() + etat.slice(1) : "—";
|
||||
return {
|
||||
label,
|
||||
className: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
|
||||
className: "bg-slate-100 text-slate-700",
|
||||
} as { label: string; className: string };
|
||||
}
|
||||
|
||||
|
|
@ -301,11 +301,11 @@ export default function PageContrats(){
|
|||
return (
|
||||
<div className="space-y-5">
|
||||
{/* En-tête + Recherche */}
|
||||
<section className="relative overflow-visible rounded-2xl border bg-white dark:bg-slate-900 dark:border-slate-800 p-4">
|
||||
<section className="relative overflow-visible rounded-2xl border bg-white p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<h1 className="text-xl font-semibold">Contrats & Paies</h1>
|
||||
<div className="sm:ml-auto flex items-center gap-2 w-full sm:w-auto">
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-xl border dark:border-slate-800 w-full sm:w-80">
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-xl border w-full sm:w-80">
|
||||
<Search className="w-4 h-4"/>
|
||||
<input value={q} onChange={(e)=>{ setQ(e.target.value); setPage(1); }} placeholder="Référence, nom, production…" className="bg-transparent outline-none text-sm flex-1"/>
|
||||
</div>
|
||||
|
|
@ -314,7 +314,7 @@ export default function PageContrats(){
|
|||
<select
|
||||
value={selectedOrg || ""}
|
||||
onChange={(e) => { setSelectedOrg(e.target.value || null); setPage(1); }}
|
||||
className="px-3 py-2 rounded-lg border dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||||
className="px-3 py-2 rounded-lg border bg-white text-sm"
|
||||
>
|
||||
<option value="">Toutes les structures</option>
|
||||
{orgs.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
|
||||
|
|
@ -324,16 +324,16 @@ export default function PageContrats(){
|
|||
</div>
|
||||
|
||||
{/* Régime tabs */}
|
||||
<div className="mt-3 inline-flex rounded-xl border dark:border-slate-800 p-1 bg-slate-50 dark:bg-slate-800/50">
|
||||
<button onClick={()=>{ setRegime("CDDU"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='CDDU' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>CDDU</button>
|
||||
<button onClick={()=>{ setRegime("RG"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='RG' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>Régime général</button>
|
||||
<div className="mt-3 inline-flex rounded-xl border p-1 bg-slate-50">
|
||||
<button onClick={()=>{ setRegime("CDDU"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='CDDU' ? 'bg-white shadow border' : 'opacity-80'}`}>CDDU</button>
|
||||
<button onClick={()=>{ setRegime("RG"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='RG' ? 'bg-white shadow border' : 'opacity-80'}`}>Régime général</button>
|
||||
</div>
|
||||
|
||||
{/* Onglets + action */}
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<div className="inline-flex rounded-xl border dark:border-slate-800 p-1 bg-slate-50 dark:bg-slate-800/50">
|
||||
<button onClick={()=>switchTab("en_cours")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='en_cours' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>En cours</button>
|
||||
<button onClick={()=>switchTab("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>Terminés</button>
|
||||
<div className="inline-flex rounded-xl border p-1 bg-slate-50">
|
||||
<button onClick={()=>switchTab("en_cours")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='en_cours' ? 'bg-white shadow border' : 'opacity-80'}`}>En cours</button>
|
||||
<button onClick={()=>switchTab("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white shadow border' : 'opacity-80'}`}>Terminés</button>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<a
|
||||
|
|
@ -349,7 +349,7 @@ export default function PageContrats(){
|
|||
return (
|
||||
<a
|
||||
href="/contrats/nouveau/saisie-tableau"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border dark:border-slate-800 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:hover:bg-slate-800 whitespace-nowrap"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border bg-white hover:bg-slate-50 whitespace-nowrap"
|
||||
>
|
||||
<Table className="w-4 h-4" /> Saisie en tableau
|
||||
</a>
|
||||
|
|
@ -361,16 +361,16 @@ export default function PageContrats(){
|
|||
type="button"
|
||||
aria-disabled="true"
|
||||
disabled
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border dark:border-slate-800 bg-white text-slate-400 dark:text-slate-500 opacity-60 cursor-not-allowed whitespace-nowrap"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border bg-white text-slate-400 opacity-60 cursor-not-allowed whitespace-nowrap"
|
||||
>
|
||||
<Table className="w-4 h-4" /> Saisie en tableau
|
||||
</button>
|
||||
<div
|
||||
role="tooltip"
|
||||
className="pointer-events-none absolute right-0 top-full mt-2 z-10 w-64 px-3 py-2 rounded-lg bg-slate-900 text-white dark:bg-slate-800 text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||||
className="pointer-events-none absolute right-0 top-full mt-2 z-10 w-64 px-3 py-2 rounded-lg bg-slate-900 text-white text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||||
>
|
||||
Cette fonction sera bientôt disponible.
|
||||
<div className="absolute left-4 -top-1 w-2 h-2 rotate-45 bg-slate-900 dark:bg-slate-800" />
|
||||
<div className="absolute left-4 -top-1 w-2 h-2 rotate-45 bg-slate-900" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -380,13 +380,13 @@ export default function PageContrats(){
|
|||
</div>
|
||||
{status === "termines" && (
|
||||
<div className="mt-3 flex flex-col sm:flex-row gap-2 sm:items-center">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-300">Filtrer par période :</div>
|
||||
<div className="text-sm text-slate-600">Filtrer par période :</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
id="period-select"
|
||||
value={period}
|
||||
onChange={(e)=>{ setPeriod(e.target.value); setPage(1); }}
|
||||
className="px-3 py-2 rounded-lg border dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||||
className="px-3 py-2 rounded-lg border bg-white text-sm"
|
||||
>
|
||||
<optgroup label="Année">
|
||||
<option value="Y">Toute l'année</option>
|
||||
|
|
@ -419,7 +419,7 @@ export default function PageContrats(){
|
|||
<select
|
||||
value={year}
|
||||
onChange={(e)=>{ setYear(parseInt(e.target.value,10)); setPage(1); }}
|
||||
className="px-3 py-2 rounded-lg border dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||||
className="px-3 py-2 rounded-lg border bg-white text-sm"
|
||||
>
|
||||
{yearOptions.map(y => <option key={y} value={y}>{y}</option>)}
|
||||
</select>
|
||||
|
|
@ -430,7 +430,7 @@ export default function PageContrats(){
|
|||
|
||||
{/* Pagination supérieure */}
|
||||
{items.length > 0 && (
|
||||
<section className="rounded-2xl border bg-white dark:bg-slate-900 dark:border-slate-800">
|
||||
<section className="rounded-2xl border bg-white">
|
||||
<Pagination
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
|
|
@ -446,11 +446,11 @@ export default function PageContrats(){
|
|||
)}
|
||||
|
||||
{/* Tableau */}
|
||||
<section className="rounded-2xl border bg-white dark:bg-slate-900 dark:border-slate-800">
|
||||
<section className="rounded-2xl border bg-white">
|
||||
<div className="overflow-x-auto overflow-visible pb-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-slate-800 bg-slate-50/80 dark:bg-slate-800/40">
|
||||
<tr className="border-b bg-slate-50/80">
|
||||
<Th>État</Th>
|
||||
<Th>Référence</Th>
|
||||
<Th>Salarié</Th>
|
||||
|
|
@ -472,7 +472,7 @@ export default function PageContrats(){
|
|||
<tr><td colSpan={8} className="py-12 text-center text-slate-500">{status==='en_cours' ? 'Aucun contrat en cours.' : 'Aucun contrat terminé.'}</td></tr>
|
||||
) : (
|
||||
items.map((c)=> (
|
||||
<tr key={c.id} className="border-b last:border-b-0 dark:border-slate-800">
|
||||
<tr key={c.id} className="border-b last:border-b-0">
|
||||
<Td>
|
||||
{(() => { const e = safeEtat(c.etat as any); return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs whitespace-nowrap ${e.className}`}>{e.label}</span>
|
||||
|
|
@ -482,7 +482,7 @@ export default function PageContrats(){
|
|||
<div className="flex flex-col">
|
||||
<a href={detailHref(c)} className="underline font-medium">{c.reference}</a>
|
||||
{(c.is_multi_mois === true || (c.regime && c.regime.toUpperCase() === "CDDU_MULTI")) && (
|
||||
<span className="mt-1 inline-flex w-fit text-[11px] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200">Multi‑mois</span>
|
||||
<span className="mt-1 inline-flex w-fit text-[11px] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-800">Multi‑mois</span>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
|
|
@ -508,16 +508,16 @@ export default function PageContrats(){
|
|||
type="button"
|
||||
aria-disabled="true"
|
||||
disabled
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 opacity-60 cursor-not-allowed"
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border opacity-60 cursor-not-allowed"
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
<div
|
||||
role="tooltip"
|
||||
className="pointer-events-none absolute right-full top-1/2 -translate-y-1/2 z-10 mr-2 w-72 px-3 py-2 rounded-lg bg-slate-900 text-white dark:bg-slate-800 text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||||
className="pointer-events-none absolute right-full top-1/2 -translate-y-1/2 z-10 mr-2 w-72 px-3 py-2 rounded-lg bg-slate-900 text-white text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||||
>
|
||||
La modification et la duplication d'un contrat Régime général n'est pas encore possible depuis l'Espace Paie, veuillez nous contacter.
|
||||
<div className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 bg-slate-900 dark:bg-slate-800" />
|
||||
<div className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 bg-slate-900" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -527,7 +527,7 @@ export default function PageContrats(){
|
|||
return (
|
||||
<a
|
||||
href={`/contrats/${c.id}/edit`}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border hover:bg-slate-50"
|
||||
aria-label="Modifier le contrat"
|
||||
title="Modifier le contrat"
|
||||
>
|
||||
|
|
@ -546,16 +546,16 @@ export default function PageContrats(){
|
|||
type="button"
|
||||
disabled
|
||||
aria-disabled="true"
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 opacity-60 cursor-not-allowed"
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border opacity-60 cursor-not-allowed"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</button>
|
||||
<div
|
||||
role="tooltip"
|
||||
className="pointer-events-none absolute right-full top-1/2 -translate-y-1/2 z-10 mr-2 w-72 px-3 py-2 rounded-lg bg-slate-900 text-white dark:bg-slate-800 text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||||
className="pointer-events-none absolute right-full top-1/2 -translate-y-1/2 z-10 mr-2 w-72 px-3 py-2 rounded-lg bg-slate-900 text-white text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||||
>
|
||||
La modification et la duplication d'un contrat Régime général n'est pas encore possible depuis l'Espace Paie, veuillez nous contacter.
|
||||
<div className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 bg-slate-900 dark:bg-slate-800" />
|
||||
<div className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 bg-slate-900" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -565,7 +565,7 @@ export default function PageContrats(){
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/contrats/nouveau?dupe_id=${encodeURIComponent(c.id)}`)}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border hover:bg-slate-50"
|
||||
aria-label="Dupliquer le contrat"
|
||||
title="Dupliquer le contrat"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -325,4 +325,4 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ export default async function StaffClientsPage({ searchParams }: { searchParams?
|
|||
);
|
||||
}
|
||||
|
||||
let query = sb.from("organizations").select("id, name, structure_api, created_at").order("created_at", { ascending: false });
|
||||
let query = sb.from("organizations").select("id, name, structure_api, created_at").order("name", { ascending: true });
|
||||
if (q) {
|
||||
// recherche simple sur name et structure_api
|
||||
query = query.ilike("name", `%${q}%`).ilike("structure_api", `%${q}%`);
|
||||
|
|
|
|||
|
|
@ -807,8 +807,12 @@ function NotifyContributionModal({ period, orgDetails, isRenotification, lastNot
|
|||
<p className="text-xs font-medium text-slate-700 mb-2">Contenu de l'email :</p>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
• Période concernée : <strong>{period.period_label}</strong><br />
|
||||
• Dates de prélèvement : <strong>{collectionPeriod}</strong><br />
|
||||
• Rappel que les montants seront prélevés directement sur le compte bancaire
|
||||
• Date de prélèvement théorique : <strong>{collectionPeriod}</strong><br />
|
||||
{period.total_due === 0 ? (
|
||||
<>• Message : En l'absence de paie pour cette période, aucune cotisation ne sera prélevée</>
|
||||
) : (
|
||||
<>• Message : Rappel des prélèvements de cotisations à venir</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,25 @@ export async function POST(
|
|||
// 8) Récupérer le code employeur
|
||||
const codeEmployeur = orgDetails.code_employeur || "N/A";
|
||||
|
||||
// 9) Préparer les données pour l'email
|
||||
// 9) Récupérer toutes les cotisations de la période pour calculer le total
|
||||
const { data: allContributions, error: contributionsError } = await supabase
|
||||
.from("monthly_contributions")
|
||||
.select("amount_due")
|
||||
.eq("org_id", cotisation.org_id)
|
||||
.eq("period_label", periodLabel);
|
||||
|
||||
if (contributionsError) {
|
||||
console.error("[notify-contribution] Erreur lors de la récupération des cotisations:", contributionsError);
|
||||
}
|
||||
|
||||
// Calculer le total des cotisations pour la période
|
||||
const totalContributions = (allContributions || []).reduce((sum, contrib) => sum + (contrib.amount_due || 0), 0);
|
||||
const hasZeroContributions = totalContributions === 0;
|
||||
|
||||
console.log("[notify-contribution] Total des cotisations:", totalContributions);
|
||||
console.log("[notify-contribution] hasZeroContributions:", hasZeroContributions);
|
||||
|
||||
// 10) Préparer les données pour l'email
|
||||
const emailData = {
|
||||
firstName,
|
||||
organizationName: organization.name,
|
||||
|
|
@ -185,9 +203,10 @@ export async function POST(
|
|||
handlerName: "Renaud BREVIERE-ABRAHAM",
|
||||
periodLabel: periodLabel,
|
||||
collectionPeriod: collectionPeriod,
|
||||
hasZeroContributions: hasZeroContributions,
|
||||
};
|
||||
|
||||
// 10) Envoyer l'email via le système universel V2
|
||||
// 11) Envoyer l'email via le système universel V2
|
||||
console.log("[notify-contribution] Envoi de l'email à:", emailNotifs);
|
||||
console.log("[notify-contribution] CC:", validatedCcEmail || "Aucun");
|
||||
|
||||
|
|
@ -200,7 +219,7 @@ export async function POST(
|
|||
|
||||
console.log("[notify-contribution] Email de notification de cotisations envoyé avec succès");
|
||||
|
||||
// 11) Enregistrer la notification dans la table contribution_notifications
|
||||
// 12) Enregistrer la notification dans la table contribution_notifications
|
||||
console.log("[notify-contribution] Tentative d'insertion dans contribution_notifications:", {
|
||||
org_id: cotisation.org_id,
|
||||
period_label: periodLabel,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import "./globals.css";
|
|||
import "@/styles/cmdk.css";
|
||||
import Providers from "@/components/Providers";
|
||||
import ProgressBar from "@/components/ProgressBar";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { PostHogPageView } from "@/components/PostHogPageView";
|
||||
import PostHogIdentifier from "@/components/PostHogIdentifier";
|
||||
import { useEffect, Suspense } from "react";
|
||||
|
||||
/**
|
||||
* Simple garde pour maintenir le company_name dans localStorage pour l'affichage UI.
|
||||
|
|
@ -65,9 +66,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||
<ProgressBar />
|
||||
{/* Garde simple pour préserver l'affichage UI */}
|
||||
<OrgPersistenceGuard />
|
||||
<Providers>{children}</Providers>
|
||||
{/* BugReporter temporairement masqué */}
|
||||
<Providers>
|
||||
<Suspense fallback={null}>
|
||||
<PostHogPageView />
|
||||
<PostHogIdentifier />
|
||||
</Suspense>
|
||||
{children}
|
||||
</Providers>
|
||||
{/* BugReporter temporairement masqué */}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,9 +95,8 @@ export default function InviteForm({ orgs, defaultOrgId, hideOrgSelect }: { orgs
|
|||
<option value="COMPTA">Compta</option>
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Le nouveau compte sera créé par <em>invitation e-mail</em>. Par défaut, il pourra se connecter par lien magique.
|
||||
Le nouveau compte sera créé par <em>invitation e-mail</em>. Par défaut, il pourra se connecter par envoi de code.
|
||||
</p>
|
||||
{hideOrgSelect && <p className="mt-1 text-xs text-amber-600">En tant que client, vous ne pouvez pas créer de Super Admin.</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { LogOut } from "lucide-react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export default function LogoutButton({ variant = "default", className }: { variant?: "default" | "compact"; className?: string }) {
|
||||
const router = useRouter();
|
||||
const posthog = usePostHog();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
|
@ -25,7 +27,11 @@ export default function LogoutButton({ variant = "default", className }: { varia
|
|||
console.warn("Erreur lors de la déconnexion:", error);
|
||||
}
|
||||
|
||||
// 2. Nettoyage complet côté client (au cas où il resterait des données)
|
||||
// 2. Réinitialiser l'identité PostHog
|
||||
posthog?.reset();
|
||||
console.log('👋 PostHog: Utilisateur déconnecté');
|
||||
|
||||
// 3. Nettoyage complet côté client (au cas où il resterait des données)
|
||||
try {
|
||||
// Nettoyer localStorage
|
||||
localStorage.removeItem("company_name");
|
||||
|
|
@ -42,7 +48,7 @@ export default function LogoutButton({ variant = "default", className }: { varia
|
|||
console.warn("Erreur lors du nettoyage local:", error);
|
||||
}
|
||||
|
||||
// 3. Redirection vers signin
|
||||
// 4. Redirection vers signin
|
||||
router.push("/signin");
|
||||
}
|
||||
|
||||
|
|
|
|||
46
components/PostHogIdentifier.tsx
Normal file
46
components/PostHogIdentifier.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
/**
|
||||
* Composant pour identifier l'utilisateur dans PostHog
|
||||
* À placer dans le layout après l'authentification
|
||||
*/
|
||||
export default function PostHogIdentifier() {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/me", { credentials: "include", cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
// Si pas authentifié, on réinitialise l'identité
|
||||
posthog?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const me = await res.json();
|
||||
|
||||
if (me?.id) {
|
||||
// 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,
|
||||
is_staff: me.is_staff || false,
|
||||
// Ajoutez d'autres propriétés utiles pour vos analytics
|
||||
});
|
||||
|
||||
console.log('👤 PostHog: Utilisateur identifié:', me.email);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ PostHog: Erreur lors de l\'identification:', e);
|
||||
}
|
||||
})();
|
||||
}, [posthog]);
|
||||
|
||||
return null;
|
||||
}
|
||||
26
components/PostHogPageView.tsx
Normal file
26
components/PostHogPageView.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
'use client';
|
||||
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { usePostHog } from 'posthog-js/react';
|
||||
|
||||
export function PostHogPageView(): null {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
// Tracker les changements de page
|
||||
if (pathname && posthog) {
|
||||
let url = window.origin + pathname;
|
||||
if (searchParams && searchParams.toString()) {
|
||||
url = url + '?' + searchParams.toString();
|
||||
}
|
||||
posthog.capture('$pageview', {
|
||||
$current_url: url,
|
||||
});
|
||||
}
|
||||
}, [pathname, searchParams, posthog]);
|
||||
|
||||
return null;
|
||||
}
|
||||
43
components/PostHogProvider.tsx
Normal file
43
components/PostHogProvider.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
'use client';
|
||||
|
||||
import posthog from 'posthog-js';
|
||||
import { PostHogProvider as PHProvider } from 'posthog-js/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
// Initialiser PostHog uniquement côté client et une seule fois
|
||||
if (typeof window !== 'undefined' && !posthog.__loaded) {
|
||||
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
const host = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
||||
|
||||
// Debug: vérifier que les variables d'environnement sont bien chargées
|
||||
console.log('🔍 PostHog - Initialisation...');
|
||||
console.log('🔑 Key:', key ? `${key.substring(0, 10)}...` : '❌ MANQUANTE');
|
||||
console.log('🌐 Host:', host || '❌ MANQUANT');
|
||||
|
||||
if (!key || !host) {
|
||||
console.error('❌ PostHog: Variables d\'environnement manquantes. Avez-vous redémarré le serveur après avoir ajouté NEXT_PUBLIC_POSTHOG_KEY et NEXT_PUBLIC_POSTHOG_HOST ?');
|
||||
return;
|
||||
}
|
||||
|
||||
posthog.init(key, {
|
||||
api_host: host,
|
||||
person_profiles: 'identified_only',
|
||||
capture_pageview: false, // On gère les pageviews manuellement avec PostHogPageView
|
||||
capture_pageleave: true,
|
||||
loaded: (posthog) => {
|
||||
console.log('✅ PostHog initialisé avec succès!');
|
||||
// Rendre posthog accessible globalement pour les tests en dev
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
posthog.debug(true); // Active les logs en dev
|
||||
(window as any).posthog = posthog; // Exposer pour les tests console
|
||||
console.log('💡 Vous pouvez maintenant utiliser "window.posthog" dans la console');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <PHProvider client={posthog}>{children}</PHProvider>;
|
||||
}
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
"use client";
|
||||
"use client";
|
||||
|
||||
import GlobalSearchOverlay from "@/components/GlobalSearchOverlay";
|
||||
import MobileSidebarOverlay from "@/components/MobileSidebarOverlay";
|
||||
import { ReactNode } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { PostHogProvider } from "@/components/PostHogProvider";
|
||||
|
||||
const qc = new QueryClient();
|
||||
export default function Providers({ children }: { children: ReactNode }){
|
||||
|
||||
export default function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={qc}>
|
||||
{children}
|
||||
<GlobalSearchOverlay />
|
||||
<MobileSidebarOverlay />
|
||||
</QueryClientProvider>
|
||||
<PostHogProvider>
|
||||
<QueryClientProvider client={qc}>
|
||||
{children}
|
||||
<GlobalSearchOverlay />
|
||||
<MobileSidebarOverlay />
|
||||
</QueryClientProvider>
|
||||
</PostHogProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -764,8 +764,8 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
|
|||
subject: 'Échéance de cotisations - {{periodLabel}}',
|
||||
title: 'Échéance de cotisations',
|
||||
greeting: '{{#if firstName}}👋 Bonjour {{firstName}},{{/if}}',
|
||||
mainMessage: 'Nous vous rappelons que les prélèvements des cotisations sociales pour la période {{periodLabel}} auront lieu prochainement.<br><br>Les montants seront prélevés directement sur votre compte bancaire entre le 15 et le 30 du mois suivant la période concernée, conformément aux échéances fixées par les organismes sociaux (URSSAF, France Travail, Audiens, etc.).',
|
||||
closingMessage: 'Assurez-vous que votre compte dispose de la provision nécessaire pour ces prélèvements.<br><br>Les détails des cotisations sont disponibles sur votre Espace Paie dans la page Cotisations.<br><br>Si vous avez des questions concernant ces cotisations, n\'hésitez pas à nous contacter en répondant à cet e-mail.<br><br>Cordialement,<br>L\'équipe Odentas.',
|
||||
mainMessage: '{{#if hasZeroContributions}}Nous vous informons qu\'en l\'absence de paie pour la période {{periodLabel}}, aucune cotisation ne vous sera prélevée par les caisses et organismes.{{else}}Nous vous rappelons que les prélèvements des cotisations sociales pour la période {{periodLabel}} auront lieu prochainement.{{/if}}',
|
||||
closingMessage: 'Si vous avez des questions concernant ces cotisations, n\'hésitez pas à nous contacter en répondant à cet e-mail.<br><br>Cordialement,<br>L\'équipe Odentas.',
|
||||
ctaText: 'Voir mes cotisations',
|
||||
ctaUrl: 'https://paie.odentas.fr/cotisations',
|
||||
footerText: 'Vous recevez cet e-mail car vous êtes client de Odentas, pour vous notifier d\'une action sur votre compte.',
|
||||
|
|
@ -789,7 +789,7 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
|
|||
title: 'Informations sur la période',
|
||||
rows: [
|
||||
{ label: 'Période concernée', key: 'periodLabel' },
|
||||
{ label: 'Dates de prélèvement', key: 'collectionPeriod' },
|
||||
{ label: 'Date de prélèvement théorique', key: 'collectionPeriod' },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
11
lib/posthog.ts
Normal file
11
lib/posthog.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { PostHog } from "posthog-node"
|
||||
|
||||
// NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog.
|
||||
export default function PostHogClient() {
|
||||
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
flushAt: 1,
|
||||
flushInterval: 0,
|
||||
})
|
||||
return posthogClient
|
||||
}
|
||||
|
|
@ -27,5 +27,21 @@ const nextConfig = {
|
|||
}
|
||||
return config;
|
||||
},
|
||||
// Rewrites pour proxier les requêtes PostHog
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/ingest/static/:path*',
|
||||
destination: 'https://eu-assets.i.posthog.com/static/:path*',
|
||||
},
|
||||
{
|
||||
source: '/ingest/:path*',
|
||||
destination: 'https://eu.i.posthog.com/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
// Nécessaire pour supporter les requêtes API PostHog avec slash final
|
||||
skipTrailingSlashRedirect: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
78
package-lock.json
generated
78
package-lock.json
generated
|
|
@ -28,6 +28,8 @@
|
|||
"lucide-react": "^0.460.0",
|
||||
"next": "^14.2.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"posthog-js": "^1.275.1",
|
||||
"posthog-node": "^5.9.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
@ -1667,6 +1669,12 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.4.tgz",
|
||||
"integrity": "sha512-o2TkycuV98PtAkcqE8B1DJv5LBvHEDTWirK5TlkQMeF2MJg0BYliY95CeRZFILNgZJCbI3k/fhahSMRQlpXOMg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
|
|
@ -4375,6 +4383,17 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
|
||||
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -5560,6 +5579,12 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
|
|
@ -7595,6 +7620,53 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.275.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.275.1.tgz",
|
||||
"integrity": "sha512-ILglAzeUQl7h7rB3axr5rn5j2wBp53XedzJoUha5IC594BsrScdOD9NjLpkDAqV/Q5IsRKXbYOkr+HKaxgb4FA==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.2.4",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rrweb/types": "2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "2.0.0-alpha.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rrweb/types": {
|
||||
"optional": true
|
||||
},
|
||||
"rrweb-snapshot": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "5.9.5",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.9.5.tgz",
|
||||
"integrity": "sha512-Rv82jMVhnxlBNf8wDbP+iAJdZrhU0aHul0LaFrQ/JGxxDiK3EkclIqr+QUwA9CulleTtXf6AIFz22tLvbVs/HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.27.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
|
||||
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -9239,6 +9311,12 @@
|
|||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
|
||||
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
"lucide-react": "^0.460.0",
|
||||
"next": "^14.2.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"posthog-js": "^1.275.1",
|
||||
"posthog-node": "^5.9.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue