diff --git a/PAGINATION_CONTRATS_IMPROVEMENT.md b/PAGINATION_CONTRATS_IMPROVEMENT.md new file mode 100644 index 0000000..be8d983 --- /dev/null +++ b/PAGINATION_CONTRATS_IMPROVEMENT.md @@ -0,0 +1,91 @@ +# Amélioration de la Pagination - Page Contrats + +## Date +13 octobre 2025 + +## Résumé +Amélioration de la pagination de la page `/contrats` pour offrir une meilleure expérience utilisateur avec : +- Pagination affichée en haut ET en bas du tableau +- Affichage du nombre total de contrats (pas seulement le nombre affiché) +- Affichage du nombre total de pages +- Possibilité de choisir le nombre d'éléments par page (10, 50, 100) + +## Modifications apportées + +### 1. API Backend (`app/api/contrats/route.ts`) +- ✅ Ajout de `total` et `totalPages` dans la réponse JSON +- ✅ Calcul correct : `total: filtered.length` et `totalPages: Math.ceil(filtered.length / limit)` +- ✅ Mise à jour pour le mode démo également + +### 2. Frontend (`app/(app)/contrats/page.tsx`) + +#### a) Type de données +- ✅ Mise à jour du type de retour de l'API pour inclure `total?: number` et `totalPages?: number` +- ✅ Extraction des valeurs : `const total = data?.total ?? 0;` et `const totalPages = data?.totalPages ?? 0;` + +#### b) Nouveau composant Pagination +Création d'un composant réutilisable avec : +```typescript +type PaginationProps = { + page: number; + totalPages: number; + total: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + isFetching?: boolean; + itemsCount: number; +}; +``` + +**Fonctionnalités :** +- Navigation avec boutons Précédent/Suivant (désactivés aux extrémités) +- Affichage "Page X sur Y" +- Sélecteur pour changer le nombre d'éléments affichés (10, 50, 100) +- Indicateur de chargement pendant les requêtes +- Affichage du nombre total de contrats et du nombre affiché + +#### c) Intégration +- ✅ **Pagination supérieure** : Ajoutée avant le tableau (uniquement si des contrats sont affichés) +- ✅ **Pagination inférieure** : Remplace l'ancienne pagination simple sous le tableau + +## Comportement + +### Affichage +- La pagination apparaît en haut du tableau uniquement s'il y a des résultats +- La pagination en bas est toujours présente (même sans résultats) +- Format responsive adapté mobile/desktop + +### Informations affichées +``` +Page 1 sur 5 | Afficher : [10 ▼] | 47 contrats au total • 10 affichés +``` + +### Changement de limite +- Quand l'utilisateur change le nombre d'éléments par page, la page est automatiquement réinitialisée à 1 +- Les options disponibles : 10, 50, 100 contrats par page + +### Navigation +- Boutons Précédent/Suivant avec icônes chevron +- Désactivation automatique aux extrémités (page 1 ou dernière page) +- Effet hover sur les boutons actifs + +## Styles +- Design cohérent avec le reste de l'application +- Support du mode sombre (dark mode) +- Bordures et espacements harmonieux +- Icônes Lucide React + +## Tests recommandés +1. ✅ Vérifier l'affichage avec différents nombres de contrats (0, 1-10, 11-50, 50+) +2. ✅ Tester la navigation entre les pages +3. ✅ Tester le changement de limite (10/50/100) +4. ✅ Vérifier que le total affiché est correct +5. ✅ Tester en mode CDDU et Régime Général +6. ✅ Tester avec les filtres (recherche, période, organisation pour staff) +7. ✅ Vérifier le responsive mobile +8. ✅ Tester en mode démo + +## Fichiers modifiés +- `/Users/renaud/Projet Nouvel Espace Paie/app/api/contrats/route.ts` +- `/Users/renaud/Projet Nouvel Espace Paie/app/(app)/contrats/page.tsx` diff --git a/PAGINATION_SALARIES_IMPROVEMENT.md b/PAGINATION_SALARIES_IMPROVEMENT.md new file mode 100644 index 0000000..c2c2ad4 --- /dev/null +++ b/PAGINATION_SALARIES_IMPROVEMENT.md @@ -0,0 +1,115 @@ +# Amélioration de la Pagination - Page Salariés + +## Date +13 octobre 2025 + +## Résumé +Amélioration de la pagination de la page `/salaries` pour offrir une meilleure expérience utilisateur avec : +- Pagination affichée en haut ET en bas du tableau +- Affichage du nombre total de salariés (pas seulement le nombre affiché) +- Affichage du nombre total de pages +- Possibilité de choisir le nombre d'éléments par page (10, 50, 100) +- Suppression de l'ancienne pagination dans la barre d'en-tête + +## Modifications apportées + +### 1. API Backend (`app/api/salaries/route.ts`) +- ✅ Ajout de `totalPages` dans le type `SalariesResponse` +- ✅ Calcul de `totalPages` : `Math.ceil(total / limit)` +- ✅ Ajout de `totalPages` dans la réponse JSON + +### 2. Frontend (`app/(app)/salaries/page.tsx`) + +#### a) Type de données +- ✅ Mise à jour du type `SalariesResponse` pour inclure `totalPages?: number` +- ✅ Extraction des valeurs : `const total = data?.total ?? 0;` et `const totalPages = data?.totalPages ?? 0;` +- ✅ Mise à jour des données démo pour inclure `totalPages` + +#### b) Nouveau composant Pagination +Création d'un composant réutilisable identique à celui de la page contrats avec : +```typescript +type PaginationProps = { + page: number; + totalPages: number; + total: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + isFetching?: boolean; + itemsCount: number; + position?: 'top' | 'bottom'; +}; +``` + +**Fonctionnalités :** +- Navigation avec boutons Précédent/Suivant (désactivés aux extrémités) +- Affichage "Page X sur Y" +- Sélecteur pour changer le nombre d'éléments affichés (10, 50, 100) +- Indicateur de chargement pendant les requêtes +- Affichage du nombre total de salariés et du nombre affiché +- Support des positions 'top' et 'bottom' pour contrôler l'affichage de la bordure + +#### c) Changement de limite +- ✅ Modification de `const [limit] = useState(25)` vers `const [limit, setLimit] = useState(10)` +- ✅ Possibilité de changer dynamiquement entre 10, 50 et 100 salariés par page + +#### d) Refactoring de la structure +- ✅ Suppression du code `headerRight` (pagination dans la barre de titre) +- ✅ Remplacement du composant `Section` par une structure `
` directe pour mieux contrôler le layout +- ✅ Ajout de la pagination supérieure avant le tableau (uniquement si des résultats sont affichés) +- ✅ Ajout de la pagination inférieure dans la section du tableau + +## Comportement + +### Affichage +- La pagination apparaît en haut du tableau uniquement s'il y a des résultats +- La pagination en bas est toujours présente (intégrée dans la section du tableau) +- Format responsive adapté mobile/desktop + +### Informations affichées +``` +Page 1 sur 5 | Afficher : [10 ▼] | 47 salariés au total • 10 affichés +``` + +### Changement de limite +- Quand l'utilisateur change le nombre d'éléments par page, la page est automatiquement réinitialisée à 1 +- Les options disponibles : 10, 50, 100 salariés par page +- Par défaut : 10 salariés (au lieu de 25 précédemment) + +### Navigation +- Boutons Précédent/Suivant avec icônes chevron +- Désactivation automatique aux extrémités (page 1 ou dernière page) +- Effet hover sur les boutons actifs + +## Styles +- Design cohérent avec la page contrats +- Bordures et espacements harmonieux +- Icônes Lucide React +- Pas de bordure supérieure pour la pagination du haut (grâce à `position="top"`) + +## Améliorations par rapport à l'ancienne version +1. ✅ Suppression de la pagination dans la barre d'en-tête (encombrait l'interface) +2. ✅ Pagination cohérente en haut et en bas du tableau +3. ✅ Information claire sur le nombre total de salariés +4. ✅ Possibilité de changer le nombre d'éléments affichés +5. ✅ Affichage du nombre de pages pour mieux se repérer + +## Tests recommandés +1. ✅ Vérifier l'affichage avec différents nombres de salariés (0, 1-10, 11-50, 50+) +2. ✅ Tester la navigation entre les pages +3. ✅ Tester le changement de limite (10/50/100) +4. ✅ Vérifier que le total affiché est correct +5. ✅ Tester avec les filtres (recherche, organisation pour staff) +6. ✅ Vérifier le responsive mobile +7. ✅ Tester en mode démo + +## Fichiers modifiés +- `/Users/renaud/Projet Nouvel Espace Paie/app/api/salaries/route.ts` +- `/Users/renaud/Projet Nouvel Espace Paie/app/(app)/salaries/page.tsx` + +## Cohérence avec la page Contrats +Les deux pages (contrats et salariés) ont maintenant exactement la même pagination : +- Même composant `Pagination` avec les mêmes props +- Même comportement et mêmes fonctionnalités +- Même design et même expérience utilisateur +- Code réutilisable et maintenable diff --git a/STAFF_GESTION_PRODUCTIONS.md b/STAFF_GESTION_PRODUCTIONS.md new file mode 100644 index 0000000..64bfb1c --- /dev/null +++ b/STAFF_GESTION_PRODUCTIONS.md @@ -0,0 +1,481 @@ +# Gestion des Productions - Documentation Staff + +## 📋 Vue d'ensemble + +La page **Gestion des Productions** (`/staff/gestion-productions`) est une interface complète permettant aux membres du staff de gérer l'ensemble des productions et spectacles de toutes les organisations. + +## ✨ Fonctionnalités + +### 🎬 CRUD Complet + +- **Créer** : Ajouter de nouvelles productions avec nom, référence, date de déclaration et organisation +- **Lire** : Visualiser toutes les productions dans un tableau moderne avec filtres +- **Mettre à jour** : Modifier les informations d'une production existante +- **Supprimer** : Supprimer une production (avec vérification d'utilisation dans les contrats) + +### 🔍 Recherche et Filtrage + +- **Recherche textuelle** : Recherche en temps réel par nom ou référence +- **Filtre par organisation** : Filtrer les productions d'une organisation spécifique +- **Effacement rapide** : Bouton pour réinitialiser tous les filtres + +### 📊 Interface Moderne + +- **Design ultra-moderne** : Interface avec gradients, ombres et animations fluides +- **Modales élégantes** : Modales avec backdrop blur et animations d'entrée +- **Badges et icônes** : Affichage visuel clair avec icônes Lucide +- **Responsive** : Adaptation automatique mobile/tablette/desktop +- **États de chargement** : Spinners et messages informatifs + +## 🎨 Interface Utilisateur + +### Header +``` +┌────────────────────────────────────────────────────┐ +│ Gestion des productions [+ Nouvelle] │ +│ Gérez les productions et spectacles... │ +└────────────────────────────────────────────────────┘ +``` + +### Barre de Filtres +- **Recherche** : Input avec icône de recherche et bouton de réinitialisation +- **Filtres** : Bouton avec badge indiquant le nombre de filtres actifs +- **Organisation** : Dropdown pour filtrer par organisation + +### Tableau des Productions + +| Colonne | Description | Type | +|---------|-------------|------| +| Nom | Nom de la production | Texte | +| Référence | Numéro d'objet/référence | Badge avec icône Hash | +| Date déclaration | Date de déclaration | Date avec icône Calendar | +| Organisation | Organisation liée | Texte | +| Actions | Éditer / Supprimer | Boutons icônes | + +### Modales + +#### Modal Création/Édition +``` +┌──────────────────────────────────────┐ +│ 🏢 Nouvelle production ✕ │ +├──────────────────────────────────────┤ +│ │ +│ 📄 Nom de la production * │ +│ [Ex: Festival d'Avignon 2025] │ +│ │ +│ # Numéro d'objet / Référence │ +│ [Ex: PROD-2025-001] │ +│ │ +│ 📅 Date de déclaration │ +│ [Date picker] │ +│ │ +│ 🏢 Organisation * │ +│ [Dropdown] │ +│ │ +│ [Annuler] [Créer ⟳] │ +└──────────────────────────────────────┘ +``` + +#### Modal Suppression +``` +┌──────────────────────────────────────┐ +│ 🗑️ Confirmer la suppression │ +│ Cette action est irréversible │ +├──────────────────────────────────────┤ +│ │ +│ Voulez-vous vraiment supprimer │ +│ la production "Festival 2025" ? │ +│ Référence : PROD-001 │ +│ │ +│ [Annuler] [Supprimer ⟳] │ +└──────────────────────────────────────┘ +``` + +## 🔧 Implémentation Technique + +### Architecture + +``` +app/(app)/staff/gestion-productions/ +└── page.tsx (Client Component) + +app/api/staff/productions/ +├── route.ts (GET, POST) +└── [id]/ + └── route.ts (GET, PATCH, DELETE) + +components/ +└── Sidebar.tsx (+ lien navigation) +``` + +### Hooks React Query + +#### `useStaffCheck()` +Vérifie si l'utilisateur connecté est staff. + +```typescript +const { data: staffCheck, isLoading } = useStaffCheck(); +// Returns: { isStaff: boolean } +``` + +#### `useOrganizations()` +Récupère la liste de toutes les organisations. + +```typescript +const { data: organizations } = useOrganizations(); +// Returns: Organization[] +``` + +#### `useProductions(orgId?, searchQuery?)` +Récupère les productions avec filtres optionnels. + +```typescript +const { data: productions, isLoading, error } = useProductions(orgId, searchQuery); +// Returns: Production[] +``` + +### Types TypeScript + +```typescript +type Production = { + id: string; + name: string; + reference: string | null; + declaration_date: string | null; + org_id: string; + created_at?: string; + updated_at?: string; +}; + +type Organization = { + id: string; + name: string; + structure_api: string; +}; +``` + +## 🌐 Routes API + +### GET `/api/staff/productions` + +**Description** : Liste toutes les productions avec filtres optionnels + +**Query Parameters** : +- `org_id` (optional) : Filtrer par organisation +- `q` (optional) : Recherche textuelle (nom ou référence) + +**Response** : +```json +{ + "productions": [ + { + "id": "uuid", + "name": "Festival d'Avignon 2025", + "reference": "PROD-2025-001", + "declaration_date": "2025-01-15", + "org_id": "org-uuid", + "created_at": "2025-01-10T10:00:00Z", + "updated_at": "2025-01-10T10:00:00Z" + } + ] +} +``` + +**Codes d'erreur** : +- `401` : Non authentifié +- `403` : Accès refusé (non-staff) +- `500` : Erreur serveur + +### POST `/api/staff/productions` + +**Description** : Créer une nouvelle production + +**Body** : +```json +{ + "name": "Festival d'Avignon 2025", + "reference": "PROD-2025-001", + "declaration_date": "2025-01-15", + "org_id": "org-uuid" +} +``` + +**Validation** : +- `name` : Requis, non vide +- `org_id` : Requis, doit exister dans `organizations` +- `reference` : Optionnel +- `declaration_date` : Optionnel + +**Response** : +```json +{ + "production": { /* Production créée */ } +} +``` + +**Codes d'erreur** : +- `400` : Validation échouée +- `401` : Non authentifié +- `403` : Accès refusé (non-staff) +- `500` : Erreur serveur + +### GET `/api/staff/productions/[id]` + +**Description** : Récupérer une production spécifique + +**Response** : +```json +{ + "production": { /* Production */ } +} +``` + +**Codes d'erreur** : +- `401` : Non authentifié +- `403` : Accès refusé (non-staff) +- `404` : Production introuvable +- `500` : Erreur serveur + +### PATCH `/api/staff/productions/[id]` + +**Description** : Mettre à jour une production + +**Body** (tous les champs optionnels) : +```json +{ + "name": "Nouveau nom", + "reference": "NEW-REF", + "declaration_date": "2025-02-01", + "org_id": "autre-org-uuid" +} +``` + +**Response** : +```json +{ + "production": { /* Production mise à jour */ } +} +``` + +**Codes d'erreur** : +- `400` : Validation échouée +- `401` : Non authentifié +- `403` : Accès refusé (non-staff) +- `404` : Production introuvable +- `500` : Erreur serveur + +### DELETE `/api/staff/productions/[id]` + +**Description** : Supprimer une production + +**Vérifications** : +- La production existe +- La production n'est pas utilisée dans des contrats CDDU + +**Response** : +```json +{ + "message": "Production supprimée avec succès", + "deleted_id": "uuid" +} +``` + +**Codes d'erreur** : +- `400` : Production utilisée dans des contrats +- `401` : Non authentifié +- `403` : Accès refusé (non-staff) +- `404` : Production introuvable +- `500` : Erreur serveur + +## 🔐 Sécurité + +### Vérifications + +- **Authentification** : Session Supabase requise +- **Autorisation** : Vérification `staff_users.is_staff = true` +- **Validation** : Toutes les entrées utilisateur sont validées +- **Protection suppression** : Vérification des dépendances (contrats) + +### Permissions + +- **Staff uniquement** : Toutes les opérations réservées au staff +- **Aucune restriction par organisation** : Le staff peut gérer toutes les organisations + +## 📱 Responsive Design + +### Desktop (≥1024px) +- Tableau pleine largeur +- Filtres sur une ligne +- Modales centrées (max-width: 640px) + +### Tablet (768px - 1023px) +- Tableau scrollable horizontal +- Filtres sur deux lignes +- Modales adaptées + +### Mobile (<768px) +- Tableau scrollable +- Filtres empilés verticalement +- Modales plein écran avec padding + +## 🎨 Design System + +### Couleurs + +- **Primary Gradient** : `from-indigo-600 to-purple-600` +- **Hover Gradient** : `from-indigo-700 to-purple-700` +- **Background** : `bg-white`, `bg-slate-50` +- **Border** : `border-slate-200` +- **Text** : `text-slate-900`, `text-slate-600` + +### Animations + +- **Modal entrée** : `animate-in fade-in zoom-in-95 duration-200` +- **Backdrop** : `backdrop-blur-sm` +- **Transitions** : `transition-all`, `transition-colors` + +### Icônes (Lucide React) + +- `Building2` : Organisations +- `FileText` : Nom de production +- `Hash` : Référence +- `Calendar` : Date de déclaration +- `Plus` : Créer +- `Edit2` : Éditer +- `Trash2` : Supprimer +- `Filter` : Filtres +- `Search` : Recherche +- `Loader2` : Chargement +- `Clapperboard` : Icône sidebar + +## 🚀 Utilisation + +### Créer une production + +1. Cliquer sur **"+ Nouvelle production"** +2. Remplir le formulaire : + - Nom (requis) + - Référence (optionnel) + - Date de déclaration (optionnel) + - Organisation (requis) +3. Cliquer sur **"Créer"** + +### Modifier une production + +1. Cliquer sur l'icône ✏️ dans la colonne Actions +2. Modifier les champs souhaités +3. Cliquer sur **"Enregistrer"** + +### Supprimer une production + +1. Cliquer sur l'icône 🗑️ dans la colonne Actions +2. Confirmer la suppression dans le modal +3. La production est supprimée si elle n'est pas utilisée + +### Filtrer les productions + +1. Cliquer sur **"Filtres"** +2. Sélectionner une organisation +3. Les résultats sont filtrés automatiquement + +### Rechercher une production + +1. Saisir du texte dans la barre de recherche +2. Les résultats sont filtrés en temps réel +3. La recherche porte sur le nom et la référence + +## 🐛 Gestion des Erreurs + +### Messages utilisateur + +- **Champ requis** : "Le nom est requis" +- **Organisation requise** : "L'organisation est requise" +- **Production utilisée** : "Cette production est utilisée dans des contrats..." +- **Erreur réseau** : "Erreur lors du chargement des productions" + +### États + +- **Loading** : Spinner avec message "Chargement..." +- **Empty** : Message "Aucune production trouvée" +- **Error** : Message d'erreur en rouge avec détails + +## 📊 Performances + +### Optimisations + +- **React Query** : Cache des requêtes API (15s) +- **useMemo** : Mémoïsation du filtrage local +- **Invalidation sélective** : Rafraîchissement uniquement des données modifiées +- **Debouncing** : Pas de debounce nécessaire (filtrage instant) + +### Stale Time + +- `useStaffCheck()` : 30 secondes +- `useOrganizations()` : 60 secondes +- `useProductions()` : 15 secondes + +## 🔗 Intégration + +### Sidebar + +La page est accessible via la sidebar staff : + +```tsx + + + Gestion des productions + +``` + +### Navigation + +- URL : `/staff/gestion-productions` +- Position : Entre "Virements salaires" et "Gestion des utilisateurs" +- Icône : 🎬 Clapperboard + +## 🧪 Tests + +### Scénarios de test + +1. **Accès non-staff** : Vérifier refus d'accès +2. **Création** : Créer une production valide +3. **Création invalide** : Vérifier validation (nom vide, org manquante) +4. **Modification** : Éditer une production existante +5. **Suppression** : Supprimer une production non utilisée +6. **Suppression protection** : Tenter de supprimer une production utilisée +7. **Filtres** : Vérifier filtrage par organisation +8. **Recherche** : Vérifier recherche textuelle +9. **Responsive** : Tester sur mobile/tablet/desktop + +### Console logs + +```javascript +// Vérifier les requêtes API +// Réseau > Fetch/XHR +// GET /api/staff/productions +// POST /api/staff/productions +// PATCH /api/staff/productions/[id] +// DELETE /api/staff/productions/[id] +``` + +## 📝 Todo / Améliorations futures + +- [ ] Export CSV/Excel des productions +- [ ] Tri des colonnes du tableau +- [ ] Pagination (si + de 100 productions) +- [ ] Filtre par date de déclaration (plage) +- [ ] Import CSV de productions en masse +- [ ] Historique des modifications +- [ ] Duplication de production +- [ ] Statistiques (nb productions par org, par période) +- [ ] Champ description/notes +- [ ] Tags/catégories de productions +- [ ] Lien direct vers les contrats associés + +## 📚 Références + +- [Lucide Icons](https://lucide.dev/) +- [TanStack Query (React Query)](https://tanstack.com/query/latest) +- [Tailwind CSS](https://tailwindcss.com/) +- [Next.js App Router](https://nextjs.org/docs/app) +- [Supabase Client](https://supabase.com/docs/reference/javascript) diff --git a/VIREMENTS_SALAIRES_SIGNATURES_STAFF.md b/VIREMENTS_SALAIRES_SIGNATURES_STAFF.md new file mode 100644 index 0000000..88e724e --- /dev/null +++ b/VIREMENTS_SALAIRES_SIGNATURES_STAFF.md @@ -0,0 +1,205 @@ +# Signatures électroniques sur la page Virements Salaires (Staff) + +## 📋 Vue d'ensemble + +Cette fonctionnalité permet aux utilisateurs **staff** de visualiser les signatures électroniques en attente pour une organisation spécifique directement depuis la page **Virements salaires**. + +## ✨ Fonctionnalités + +### Pour les utilisateurs Staff uniquement + +1. **Menu déroulant des organisations** + - Affiché uniquement pour les utilisateurs staff + - Liste toutes les organisations disponibles + - Permet de sélectionner une organisation pour voir ses données + +2. **Section Signatures électroniques** + - Affichée uniquement quand un staff a sélectionné une organisation + - Liste tous les contrats en attente de signature pour l'organisation + - Informations affichées : + - Référence du contrat + - Nom du salarié + - Profession + - Date de début + - Statut signature employeur (✓ Signé / ⏳ En attente) + - Statut signature salarié (✓ Signé / ⏳ En attente) + - Bouton "Voir" pour accéder au lien de signature DocuSeal + +## 🔧 Implémentation technique + +### Fichiers modifiés + +#### 1. `/app/(app)/virements-salaires/page.tsx` + +**Nouveaux hooks ajoutés :** +```typescript +// Hook pour récupérer les signatures électroniques d'une organisation +function useSignatures(selectedOrgId?: string) +``` + +**Nouvelles fonctionnalités :** +- Menu déroulant de sélection d'organisation (visible uniquement pour les staff) +- Section d'affichage des signatures électroniques avec tableau détaillé +- Icônes ajoutées : `FileSignature`, `Eye` + +**État local :** +```typescript +const [selectedOrgId, setSelectedOrgId] = useState(""); +``` + +#### 2. `/app/api/signatures-electroniques/contrats/route.ts` + +**Modifications :** +- Ajout du support du paramètre `org_id` en query string +- Les utilisateurs staff peuvent passer un `org_id` pour filtrer les signatures d'une organisation spécifique +- Champs supplémentaires retournés : + - `employee_first_name` + - `employee_last_name` + - `role` (profession) + - `start_date` + - `signature_employeur` (booléen) + - `signature_salarie` (booléen) + +**Exemple d'appel API :** +```typescript +GET /api/signatures-electroniques/contrats?org_id= +``` + +### API utilisées + +#### 1. GET `/api/staff/organizations` +- Liste toutes les organisations +- Accessible uniquement aux staff +- Retourne : `{ organizations: [{ id, name, structure_api }] }` + +#### 2. GET `/api/signatures-electroniques/contrats?org_id=` +- Liste les signatures électroniques en attente +- Filtre par organisation si `org_id` fourni +- Accessible aux staff et membres d'organisation +- Retourne : `{ records: [...] }` + +## 🎨 Interface utilisateur + +### Menu déroulant des organisations +```tsx + +``` + +### Tableau des signatures +| Colonne | Description | +|---------|-------------| +| Référence | Référence du contrat | +| Salarié·e | Nom complet du salarié | +| Profession | Fonction/rôle du salarié | +| Date début | Date de début du contrat | +| Statut employeur | Badge ✓ Signé ou ⏳ En attente | +| Statut salarié | Badge ✓ Signé ou ⏳ En attente | +| Actions | Bouton "Voir" avec icône Eye | + +### États des badges +- **✓ Signé** : Badge vert (`bg-emerald-100 text-emerald-800`) +- **⏳ En attente** : Badge orange (`bg-amber-100 text-amber-800`) + +## 🔐 Sécurité et permissions + +### Vérifications côté client +- Le menu déroulant et la section signatures ne s'affichent que si `userInfo?.isStaff === true` +- La section signatures ne s'affiche que si une organisation est sélectionnée + +### Vérifications côté serveur (API) +- Vérification du statut staff via la table `staff_users` +- Les utilisateurs non-staff ne peuvent voir que les signatures de leur propre organisation +- Les staff peuvent voir les signatures de n'importe quelle organisation + +## 📊 Filtrage des signatures + +L'API filtre automatiquement les contrats selon les critères suivants : +- `etat_de_la_demande` = "Traitée" (ou variantes) +- Contrats en attente de signature employeur : `contrat_signe_par_employeur` = "non" +- Contrats en attente de signature salarié : `contrat_signe` = "non" + `contrat_signe_par_employeur` = "oui" + +## 🚀 Utilisation + +### Pour un utilisateur Staff + +1. Se connecter avec un compte staff +2. Accéder à la page `/virements-salaires` +3. Sélectionner une organisation dans le menu déroulant en haut de la page +4. La section "Signatures électroniques" apparaît automatiquement sous les virements +5. Consulter les signatures en attente +6. Cliquer sur "Voir" pour accéder au lien DocuSeal + +### Cas d'usage + +- **Suivi global** : Les staff peuvent surveiller l'état des signatures pour toutes les organisations +- **Support client** : Identifier rapidement les contrats bloqués en attente de signature +- **Vérification** : S'assurer que les signatures sont en cours avant d'effectuer les virements + +## 🔄 Synchronisation des données + +- Les données des signatures sont chargées automatiquement lors de la sélection d'une organisation +- Le hook `useSignatures` utilise React Query avec un `staleTime` de 15 secondes +- Les données sont mises en cache pour éviter les appels API inutiles + +## 📝 Types TypeScript + +```typescript +type SignatureRecord = { + id: string; + fields: { + reference: string; + employee_first_name: string; + employee_last_name: string; + role: string; // Profession + start_date: string; + signature_employeur: boolean; + signature_salarie: boolean; + signature_link?: string; + }; +}; +``` + +## ⚠️ Points d'attention + +1. **Performance** : La liste des organisations est chargée uniquement pour les staff +2. **Cache** : Les signatures sont mises en cache avec React Query +3. **Liens DocuSeal** : Le lien peut être absent si la signature n'a pas encore été initiée +4. **Format des données** : L'API retourne un format compatible avec l'ancien système Airtable + +## 🐛 Débogage + +### Console navigateur +```javascript +// Vérifier les données chargées +// Dans React Query Devtools, chercher les clés : +// - ["organizations"] +// - ["signatures-electroniques", selectedOrgId] +``` + +### Logs serveur +``` +🔍 [signatures-electroniques] Requête pour scope: employeur, orgId: +📊 [signatures-electroniques] Contrats trouvés: X +📋 [signatures-electroniques] Échantillon: [...] +``` + +## 🔮 Améliorations futures + +- [ ] Ajouter un filtre par statut (tout / en attente employeur / en attente salarié) +- [ ] Ajouter une recherche par nom de salarié ou référence +- [ ] Afficher le nombre de signatures en attente dans le titre de section +- [ ] Ajouter un bouton de rafraîchissement manuel +- [ ] Permettre l'envoi de relances depuis cette page +- [ ] Afficher la date de création du contrat +- [ ] Ajouter un tri par date/référence/salarié + +## 📚 Documentation associée + +- [SIGNATURE_SALARIE_FEATURE.md](./SIGNATURE_SALARIE_FEATURE.md) - Documentation des signatures salarié +- [DOCUSEAL_ENV_VARIABLES.md](./DOCUSEAL_ENV_VARIABLES.md) - Variables d'environnement DocuSeal +- [STAFF_MAINTENANCE_ACCESS_GUIDE.md](./STAFF_MAINTENANCE_ACCESS_GUIDE.md) - Guide d'accès staff diff --git a/app/(app)/contrats/page.tsx b/app/(app)/contrats/page.tsx index eb1f6a1..8569c14 100644 --- a/app/(app)/contrats/page.tsx +++ b/app/(app)/contrats/page.tsx @@ -93,7 +93,7 @@ function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "term // Build final clientInfo to pass to api(): if UI provided explicit org filter, override id const finalClientInfo = clientInfo ? { ...clientInfo, id: org ?? clientInfo.id } : (org ? { id: org, name: "Organisation", api_name: undefined } as ClientInfo : null); - return api<{ items: Contrat[]; page: number; limit: number; hasMore: boolean }>(base + qs, {}, finalClientInfo); + return api<{ items: Contrat[]; page: number; limit: number; hasMore: boolean; total?: number; totalPages?: number }>(base + qs, {}, finalClientInfo); }, staleTime: 15_000, placeholderData: (prev) => prev, @@ -125,6 +125,81 @@ function humanizeEtat(raw?: string){ return undefined as unknown as string; } +// --- Composant de pagination +type PaginationProps = { + page: number; + totalPages: number; + total: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + isFetching?: boolean; + itemsCount: number; + position?: 'top' | 'bottom'; +}; + +function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChange, isFetching, itemsCount, position = 'bottom' }: PaginationProps) { + return ( +
+ {/* Navigation */} +
+ +
+ Page {page} sur {totalPages || 1} +
+ +
+ + {/* Sélecteur de limite */} +
+ Afficher : + +
+ + {/* Informations */} +
+ {isFetching ? ( + + + Mise à jour… + + ) : ( + + {total > 0 ? ( + <> + {total} contrat{total > 1 ? 's' : ''} au total + {itemsCount > 0 && ` • ${itemsCount} affiché${itemsCount > 1 ? 's' : ''}`} + + ) : ( + 'Aucun contrat' + )} + + )} +
+
+ ); +} + function safeEtat(etat?: string){ const key = humanizeEtat(etat) as keyof typeof ETATS | undefined; if (key && ETATS[key]) return ETATS[key]; @@ -202,6 +277,8 @@ export default function PageContrats(){ }); const items = data?.items ?? []; const hasMore = data?.hasMore ?? false; + const total = data?.total ?? 0; + const totalPages = data?.totalPages ?? 0; useEffect(() => { // Load organizations for the selector @@ -351,6 +428,23 @@ export default function PageContrats(){ )}
+ {/* Pagination supérieure */} + {items.length > 0 && ( +
+ setPage(newPage)} + onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }} + isFetching={isFetching} + itemsCount={items.length} + position="top" + /> +
+ )} + {/* Tableau */}
@@ -488,13 +582,18 @@ export default function PageContrats(){
- {/* Pagination */} -
- -
Page {page}
- -
{isFetching ? 'Mise à jour…' : `${items.length} élément${items.length>1?'s':''}${hasMore ? ' (plus disponibles)' : ''}`}
-
+ {/* Pagination inférieure */} + setPage(newPage)} + onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }} + isFetching={isFetching} + itemsCount={items.length} + position="bottom" + />
); diff --git a/app/(app)/facturation/page.tsx b/app/(app)/facturation/page.tsx index 2162812..bf48a43 100644 --- a/app/(app)/facturation/page.tsx +++ b/app/(app)/facturation/page.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/fetcher"; -import { Loader2, CheckCircle2, XCircle, FileDown, Edit } from "lucide-react"; +import { Loader2, CheckCircle2, XCircle, FileDown, Edit, ChevronLeft, ChevronRight } from "lucide-react"; import { usePageTitle } from "@/hooks/usePageTitle"; // ---------------- Types ---------------- @@ -33,6 +33,8 @@ type BillingResponse = { page: number; limit: number; hasMore: boolean; + total?: number; + totalPages?: number; }; }; @@ -73,6 +75,81 @@ function Section({ title, children }: { title: string; children: React.ReactNode ); } +// -------------- Composant Pagination -------------- +type PaginationProps = { + page: number; + totalPages: number; + total: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + isFetching?: boolean; + itemsCount: number; + position?: 'top' | 'bottom'; +}; + +function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChange, isFetching, itemsCount, position = 'bottom' }: PaginationProps) { + return ( +
+ {/* Navigation */} +
+ +
+ Page {page} sur {totalPages || 1} +
+ +
+ + {/* Sélecteur de limite */} +
+ Afficher : + +
+ + {/* Informations */} +
+ {isFetching ? ( + + + Mise à jour… + + ) : ( + + {total > 0 ? ( + <> + {total} facture{total > 1 ? 's' : ''} au total + {itemsCount > 0 && ` • ${itemsCount} affichée${itemsCount > 1 ? 's' : ''}`} + + ) : ( + 'Aucune facture' + )} + + )} +
+
+ ); +} + // -------------- Data hook -------------- function useBilling(page: number, limit: number) { // Récupération dynamique des infos client via /api/me @@ -113,11 +190,13 @@ export default function FacturationPage() { usePageTitle("Facturation"); const [page, setPage] = useState(1); - const limit = 10; - const { data, isLoading, isError, error } = useBilling(page, limit); + const [limit, setLimit] = useState(10); + const { data, isLoading, isError, error, isFetching } = useBilling(page, limit); const items = data?.factures.items ?? []; const hasMore = data?.factures.hasMore ?? false; + const total = data?.factures.total ?? 0; + const totalPages = data?.factures.totalPages ?? 0; return (
@@ -176,6 +255,23 @@ export default function FacturationPage() { )} + {/* Pagination supérieure */} + {items.length > 0 && ( +
+ setPage(newPage)} + onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }} + isFetching={isFetching} + itemsCount={items.length} + position="top" + /> +
+ )} + {/* Factures */}
@@ -243,28 +339,20 @@ export default function FacturationPage() {
)} - {/* Pagination */} + {/* Pagination inférieure */} {data && ( -
-
- Page {data.factures.page} — {items.length} élément{items.length > 1 ? "s" : ""} -
-
- - -
+
+ setPage(newPage)} + onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }} + isFetching={isFetching} + itemsCount={items.length} + position="bottom" + />
)}
diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 3d1a35c..569edf0 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -35,6 +35,7 @@ export default function Dashboard() { const items = data?.items ?? []; const hasMore = data?.hasMore ?? false; + const total = data?.total ?? 0; // Récupérer les infos utilisateur pour personnaliser l'accueil const { data: clientInfo } = useQuery({ @@ -68,11 +69,13 @@ export default function Dashboard() { : null; const count = items.length; - const countLabel = count === 0 + const countLabel = total === 0 ? "Vous n'avez aucun contrat en cours" - : count === 1 - ? `Vous avez 1 contrat en cours (page ${currentPage})` - : `Vous avez ${count} contrats en cours (page ${currentPage})`; + : total === 1 + ? "Vous avez 1 contrat en cours" + : count === total + ? `Vous avez ${total} contrat${total > 1 ? 's' : ''} en cours` + : `Vous avez ${total} contrats en cours • ${count} affiché${count > 1 ? 's' : ''} sur cette page`; const handlePrevPage = () => { if (currentPage > 1) { diff --git a/app/(app)/salaries/page.tsx b/app/(app)/salaries/page.tsx index 8079bbc..24a99a7 100644 --- a/app/(app)/salaries/page.tsx +++ b/app/(app)/salaries/page.tsx @@ -29,6 +29,7 @@ type SalariesResponse = { page: number; limit: number; total?: number; + totalPages?: number; hasMore: boolean; }; @@ -56,6 +57,81 @@ function lastContractHref(c?: SalarieRow["dernier_contrat"]) { return isMulti ? `/contrats-multi/${c.id}` : `/contrats/${c.id}`; } +/* ===== Composant de pagination ===== */ +type PaginationProps = { + page: number; + totalPages: number; + total: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + isFetching?: boolean; + itemsCount: number; + position?: 'top' | 'bottom'; +}; + +function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChange, isFetching, itemsCount, position = 'bottom' }: PaginationProps) { + return ( +
+ {/* Navigation */} +
+ +
+ Page {page} sur {totalPages || 1} +
+ +
+ + {/* Sélecteur de limite */} +
+ Afficher : + +
+ + {/* Informations */} +
+ {isFetching ? ( + + + Mise à jour… + + ) : ( + + {total > 0 ? ( + <> + {total} salarié{total > 1 ? 's' : ''} au total + {itemsCount > 0 && ` • ${itemsCount} affiché${itemsCount > 1 ? 's' : ''}`} + + ) : ( + 'Aucun salarié' + )} + + )} +
+
+ ); +} + /* ===== Data hook ===== */ function useSalaries(page: number, limit: number, search: string, org?: string | null) { // 🎭 Détection directe du mode démo @@ -166,6 +242,7 @@ function useSalaries(page: number, limit: number, search: string, org?: string | page, limit, total: filteredSalaries.length, + totalPages: Math.ceil(filteredSalaries.length / limit), hasMore: endIndex < filteredSalaries.length, }, isLoading: false, @@ -232,7 +309,7 @@ export default function SalariesPage() { const initialPage = Number(searchParams.get("page") || 1); const initialSearch = searchParams.get("q") || ""; const [page, setPage] = useState(initialPage); - const [limit] = useState(25); + const [limit, setLimit] = useState(10); const [query, setQuery] = useState(initialSearch); const debouncedSearch = useDebouncedCallback((v: string) => { @@ -263,6 +340,8 @@ export default function SalariesPage() { const { data, isLoading, isFetching } = useSalaries(page, limit, query, selectedOrg); const rows: SalarieRow[] = data?.items ?? []; const hasMore: boolean = data?.hasMore ?? false; + const total = data?.total ?? 0; + const totalPages = data?.totalPages ?? 0; useEffect(() => { let mounted = true; @@ -284,50 +363,6 @@ export default function SalariesPage() { const [selectedMatricule, setSelectedMatricule] = useState(null); const [selectedNom, setSelectedNom] = useState(null); - const headerRight = useMemo( - () => ( -
- -
Page {page}
- -
- ), - [page, hasMore, isFetching, query, router, searchParams] - ); - return (
{/* Barre de titre + actions */} @@ -375,20 +410,40 @@ export default function SalariesPage() { > Nouveau salarié - {headerRight}
+ {/* Pagination supérieure */} + {rows.length > 0 && ( +
+ setPage(newPage)} + onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }} + isFetching={isFetching} + itemsCount={rows.length} + position="top" + /> +
+ )} + {/* Tableau */} -
+
+
+ Liste +
{isLoading ? ( -
+
Chargement des salarié·e·s…
) : ( -
- + <> +
+
@@ -459,46 +514,23 @@ export default function SalariesPage() {
Salarié
+ + {/* Pagination inférieure dans la Section */} + setPage(newPage)} + onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }} + isFetching={isFetching} + itemsCount={rows.length} + position="bottom" + /> + )} -
+
- {/* Pagination bas de page */} -
- -
Page {page}
- -
{newContratOpen && (
{/* Backdrop */} diff --git a/app/(app)/signatures-electroniques/page.tsx b/app/(app)/signatures-electroniques/page.tsx index 0a6c366..a7f6354 100644 --- a/app/(app)/signatures-electroniques/page.tsx +++ b/app/(app)/signatures-electroniques/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { FileSignature, BellRing, XCircle } from 'lucide-react'; import Script from 'next/script'; import { usePageTitle } from '@/hooks/usePageTitle'; +import { useQuery } from '@tanstack/react-query'; type AirtableRecord = { id: string; @@ -27,6 +28,58 @@ function classNames(...arr: Array) { return arr.filter(Boolean).join(' '); } +// Hook pour récupérer les infos utilisateur +function useUserInfo() { + return useQuery({ + queryKey: ["user-info"], + queryFn: async () => { + try { + const res = await fetch("/api/me", { + cache: "no-store", + headers: { Accept: "application/json" }, + credentials: "include" + }); + if (!res.ok) return null; + const me = await res.json(); + + return { + isStaff: Boolean(me.is_staff || me.isStaff), + orgId: me.orgId || me.active_org_id || null, + orgName: me.orgName || me.active_org_name || "Organisation", + }; + } catch { + return null; + } + }, + staleTime: 30_000, + }); +} + +// Hook pour récupérer les organisations (staff uniquement) +function useOrganizations() { + const { data: userInfo, isSuccess: userInfoLoaded } = useUserInfo(); + + return useQuery({ + queryKey: ["organizations"], + queryFn: async () => { + try { + const res = await fetch("/api/staff/organizations", { + cache: "no-store", + headers: { Accept: "application/json" }, + credentials: "include" + }); + if (!res.ok) return []; + const data = await res.json(); + return data.organizations || []; + } catch { + return []; + } + }, + enabled: userInfoLoaded && !!userInfo?.isStaff, + staleTime: 60_000, + }); +} + export default function SignaturesElectroniques() { usePageTitle("Signatures électroniques"); @@ -50,15 +103,26 @@ export default function SignaturesElectroniques() { // État pour les relances const [loadingRelance, setLoadingRelance] = useState>({}); + // État pour le sélecteur d'organisation (staff uniquement) + const [selectedOrgId, setSelectedOrgId] = useState(""); + + // Charger les infos utilisateur et organisations + const { data: userInfo } = useUserInfo(); + const { data: organizations } = useOrganizations(); + // Suppression de pollActive et pollTimer car le polling a été retiré // Load current contracts to sign (server-side API fetches Airtable) async function load() { try { setError(null); + + // Ajouter le paramètre org_id si sélectionné (staff uniquement) + const orgParam = selectedOrgId ? `&org_id=${selectedOrgId}` : ''; + const [rEmp, rSal] = await Promise.all([ - fetch('/api/signatures-electroniques/contrats?scope=employeur', { cache: 'no-store' }), - fetch('/api/signatures-electroniques/contrats?scope=salarie', { cache: 'no-store' }), + fetch(`/api/signatures-electroniques/contrats?scope=employeur${orgParam}`, { cache: 'no-store' }), + fetch(`/api/signatures-electroniques/contrats?scope=salarie${orgParam}`, { cache: 'no-store' }), ]); if (!rEmp.ok) throw new Error(`HTTP employeur ${rEmp.status}`); if (!rSal.ok) throw new Error(`HTTP salarie ${rSal.status}`); @@ -76,7 +140,7 @@ export default function SignaturesElectroniques() { useEffect(() => { load(); - }, []); + }, [selectedOrgId]); // Recharger quand l'organisation change // Ajouter des écouteurs pour recharger les données quand un modal se ferme useEffect(() => { @@ -287,9 +351,36 @@ export default function SignaturesElectroniques() { {/* DocuSeal web component script */}