feat: Ajouter tri personnalisable (date début/fin) dans la page contrats

- Ajouter les états sortField et sortOrder au composant PageContrats
- Modifier le hook useContrats pour passer sort et order à l'API
- Adapter l'endpoint /api/contrats pour supporter les paramètres de tri dynamiques
- Rendre les headers 'Début' et 'Fin' cliquables avec indicateurs visuels (▲/▼)
- Tri par défaut: date de fin décroissante (contrats les plus proches d'expirer en premier)
This commit is contained in:
odentas 2025-10-20 14:12:34 +02:00
parent 1d49bdac82
commit cd1ce09be5
3 changed files with 172 additions and 8 deletions

View file

@ -0,0 +1,135 @@
---
applyTo: '**'
---
# Instructions pour les IA Copilot - Espace Paie Odentas
## 📋 Context du Projet
**Nom du projet** : Nouvel Espace Paie Odentas
**Type** : Application Web Next.js 14 (Full-Stack)
**Langage principal** : TypeScript + React
**Base de données** : Supabase (PostgreSQL)
**Authentification** : Supabase Auth + 2FA TOTP
**Hébergement** : Vercel (région cdg1 - Paris)
## 🎯 Architecture du Projet
### Stack Technologique
- **Frontend** : Next.js 14, React 18, TypeScript, Tailwind CSS
- **Backend** : Next.js API Routes, TypeScript
- **Authentification** : Supabase Auth, TOTP 2FA
- **Base de données** : Supabase PostgreSQL
- **Stockage** : AWS S3, Supabase Storage
- **Signatures électroniques** : Docuseal
- **Analytics** : PostHog
- **PDF** : PDFMonkey
### Structure des Dossiers
```
/app → Next.js App Router (pages et layouts)
/app/api → API Routes (devraient être sur cdg1 dans vercel.json)
/components → Composants React réutilisables
/lib → Utilitaires et helpers
/hooks → Hooks React personnalisés
/templates-mails → Templates d'emails HTML
/public → Assets statiques
```
## 🔑 Points Importants
### 1. Authentification & Sécurité
- Toujours utiliser `createRouteHandlerClient` pour les routes API
- Le 2FA TOTP est activable mais optionnel
- Les statuts MFA sont : "verified" (activé) et autres (désactivé)
- **Important** : Comparer avec `!== "verified"` au lieu de `=== "unverified"` (le type n'existe pas)
### 2. Configuration Vercel
- **Région des Functions API** : cdg1 (Paris) - À MAINTENIR dans vercel.json
- Les functions ne doivent PAS être sur iad1 (risque de panne)
- Configuration dans `vercel.json` :
```json
"regions": ["cdg1"]
```
### 3. Base de Données
- Tables principales :
- `profiles` → Utilisateurs
- `organizations` → Entreprises/clients
- `employees` → Salariés
- `contracts` → Contrats CDDU et RG
- `payslips` → Fiches de paie
- `cotisations` → Cotisations mensuelles
- `salary_transfers` → Virements salaires
### 4. Régimes de Contrats
- **CDDU** : CDD d'usage (intermittents du spectacle)
- `CDDU_MONO` : Mono-mois
- `CDDU_MULTI` : Multi-mois
- **RG** : Régime Général (salaires classiques)
### 5. Composants Clés
#### Formulaire CDDU
- Fichier : `components/contrats/NouveauCDDUForm.tsx`
- Contient un **bouton calculatrice** pour saisir les montants
- Utilise le composant `Calculator` pour les calculs
- Support des deux régimes (CDDU et RG)
#### Calculatrice
- Fichier : `components/Calculator.tsx`
- Modale draggable avec focus
- **Important** : Vérifier que le focus ne capture pas les autres champs
- Utiliser `calculatorRef.current.contains(document.activeElement)` pour vérifier le focus
### 6. Hooks Personnalisés
- `useDemoMode()` → Mode démo activé/désactivé
- `usePageTitle()` → Définir le titre de la page
- `usePostHog()` → Analytics PostHog
## ✅ Standards de Code
### TypeScript
- Toujours définir les types explicitement
- Utiliser les enums pour les statuts
- Valider les types de statuts MFA avec `!==` plutôt que `===`
- **JAMAIS d'emojis dans le code** (commentaires, messages, etc.)
### Composants React
- Utiliser "use client" pour les composants interactifs
- Préférer les fonctions pures
- Gestion du focus : vérifier que le focus est bien sur l'élément avant de capturer les événements
- **Pour l'UI/UX** : Uniquement des icônes **Lucide React** (depuis `lucide-react`)
- **JAMAIS d'emojis dans l'interface utilisateur**
### Styling
- Utiliser Tailwind CSS avec les utilitaires de base
- Palette de couleurs : slate, indigo, orange, green, red
- Réutiliser les classes composables (flex, gap, etc.)
## 🐛 Corrections Récentes
- ✅ Ajout du bouton calculatrice au formulaire CDDU
- ✅ Correction des comparaisons de statut MFA (unverified → !== "verified")
- ✅ Correction du focus trap de la calculatrice
- ✅ Migration des API Functions de iad1 vers cdg1
## 📝 Conventions de Commit
- `feat:` → Nouvelle fonctionnalité
- `fix:` → Correction de bug
- `chore:` → Tâche de maintenance
- `style:` → Changements de style uniquement
- `refactor:` → Refactorisation de code
Exemple : `fix: Corriger focus trap de la calculatrice`
- Toujours proposer un git commit et un git push à la fin d'une modification.
## ⚠️ Points d'Attention
1. **Région Vercel** : Toujours vérifier que cdg1 est défini dans vercel.json
2. **Authentification** : Ne jamais exposer les tokens en client-side
3. **Focus management** : Toujours vérifier le focus avant de capturer les événements clavier
4. **Typage MFA** : Utiliser `!== "verified"` pour les comparaisons, jamais `=== "unverified"`
5. **Build local** : Tester avec `npm run build` avant de pousser

View file

@ -27,8 +27,8 @@ type ClientInfo = {
} | null; } | null;
// --- Hook d'accès API - MODIFIÉ pour récupérer clientInfo dynamiquement // --- Hook d'accès API - MODIFIÉ pour récupérer clientInfo dynamiquement
function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "termines"; page: number; limit: number; q?: string; month?: number; year?: number; period?: string; org?: string | null }){ function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "termines"; page: number; limit: number; q?: string; month?: number; year?: number; period?: string; org?: string | null; sortField?: 'date_debut' | 'date_fin'; sortOrder?: 'asc' | 'desc' }){
const { regime, status, page, limit, q, month, year, period, org } = params; const { regime, status, page, limit, q, month, year, period, org, sortField = 'date_fin', sortOrder = 'desc' } = params;
// Récupération dynamique des infos client via /api/me // Récupération dynamique des infos client via /api/me
const { data: clientInfo } = useQuery({ const { data: clientInfo } = useQuery({
@ -66,6 +66,8 @@ function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "term
status === "termines" ? year : undefined, status === "termines" ? year : undefined,
clientInfo?.id, clientInfo?.id,
org, org,
sortField,
sortOrder,
], ],
queryFn: () => { queryFn: () => {
const base = `/contrats?regime=${encodeURIComponent(regime)}&status=${status}&page=${page}&limit=${limit}`; const base = `/contrats?regime=${encodeURIComponent(regime)}&status=${status}&page=${page}&limit=${limit}`;
@ -89,6 +91,9 @@ function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "term
if (!Number.isNaN(sv)) parts.push(`semester=${sv}`); if (!Number.isNaN(sv)) parts.push(`semester=${sv}`);
} }
} }
// Ajouter les paramètres de tri
if (sortField) parts.push(`sort=${encodeURIComponent(sortField)}`);
if (sortOrder) parts.push(`order=${encodeURIComponent(sortOrder)}`);
const qs = parts.length ? `&${parts.join("&")}` : ""; const qs = parts.length ? `&${parts.join("&")}` : "";
// Build final clientInfo to pass to api(): if UI provided explicit org filter, override id // Build final clientInfo to pass to api(): if UI provided explicit org filter, override id
@ -218,6 +223,8 @@ export default function PageContrats(){
const [limit, setLimit] = useState(10); const [limit, setLimit] = useState(10);
const [q, setQ] = useState(""); const [q, setQ] = useState("");
const [regime, setRegime] = useState<"CDDU" | "RG">("CDDU"); const [regime, setRegime] = useState<"CDDU" | "RG">("CDDU");
const [sortField, setSortField] = useState<'date_debut' | 'date_fin'>('date_fin'); // Tri par défaut: date de fin
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); // Ordre par défaut: décroissant
const router = useRouter(); const router = useRouter();
// 🎭 Détection du mode démo // 🎭 Détection du mode démo
@ -283,6 +290,8 @@ export default function PageContrats(){
year: status === "termines" ? year : undefined, year: status === "termines" ? year : undefined,
period: status === "termines" ? period : undefined, period: status === "termines" ? period : undefined,
org: selectedOrg || null, org: selectedOrg || null,
sortField,
sortOrder,
}); });
const items = data?.items ?? []; const items = data?.items ?? [];
const hasMore = data?.hasMore ?? false; const hasMore = data?.hasMore ?? false;
@ -467,8 +476,12 @@ export default function PageContrats(){
<Th className="hidden sm:table-cell">Structure</Th> <Th className="hidden sm:table-cell">Structure</Th>
<Th>{regime === 'RG' ? 'Analytique' : 'Production'}</Th> <Th>{regime === 'RG' ? 'Analytique' : 'Production'}</Th>
<Th>Profession</Th> <Th>Profession</Th>
<Th>Début</Th> <Th className="cursor-pointer" onClick={() => { setSortField('date_debut'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
<Th>Fin</Th> Début {sortField === 'date_debut' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</Th>
<Th className="cursor-pointer" onClick={() => { setSortField('date_fin'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Fin {sortField === 'date_fin' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</Th>
<Th className="text-right pr-4">Actions</Th> <Th className="text-right pr-4">Actions</Th>
</tr> </tr>
</thead> </thead>

View file

@ -94,6 +94,9 @@ export async function GET(req: Request) {
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Recherche // Recherche
const q = url.searchParams.get("q")?.trim(); const q = url.searchParams.get("q")?.trim();
// Tri
const sort = url.searchParams.get("sort") || "date_fin";
const order = url.searchParams.get("order") || "desc";
// Support explicit org filter via query param `org_id` (sent by client UI when staff selects a structure). // Support explicit org filter via query param `org_id` (sent by client UI when staff selects a structure).
// Resolution priority: // Resolution priority:
// 1) If `org_id` query param is present, allow it only for staff or if it matches resolved orgId for non-staff. // 1) If `org_id` query param is present, allow it only for staff or if it matches resolved orgId for non-staff.
@ -256,11 +259,24 @@ export async function GET(req: Request) {
}); });
} }
// Si regime === null/undefined, on garde tous les contrats (pas de filtrage) // Si regime === null/undefined, on garde tous les contrats (pas de filtrage)
// Tri décroissant par date de fin (plus récent d'abord) // Tri basé sur les paramètres sort/order
filtered.sort((a: any, b: any) => { filtered.sort((a: any, b: any) => {
const aDate = new Date(a.end_date || a.date_fin); let aValue: any;
const bDate = new Date(b.end_date || b.date_fin); let bValue: any;
return bDate.getTime() - aDate.getTime();
// Déterminer le champ à trier
if (sort === 'date_debut' || sort === 'start_date') {
aValue = new Date(a.start_date || a.date_debut || 0);
bValue = new Date(b.start_date || b.date_debut || 0);
} else {
// Défaut: date_fin
aValue = new Date(a.end_date || a.date_fin || 0);
bValue = new Date(b.end_date || b.date_fin || 0);
}
// Appliquer l'ordre
const comparison = aValue.getTime() - bValue.getTime();
return order === 'asc' ? comparison : -comparison;
}); });
// Pagination JS après filtrage/tri // Pagination JS après filtrage/tri
const paged = filtered.slice(offset, offset + limit); const paged = filtered.slice(offset, offset + limit);