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:
parent
1d49bdac82
commit
cd1ce09be5
3 changed files with 172 additions and 8 deletions
135
.github/instructions/instructions.instructions.md
vendored
135
.github/instructions/instructions.instructions.md
vendored
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue