diff --git a/BULK_DELETE_CONTRATS.md b/BULK_DELETE_CONTRATS.md new file mode 100644 index 0000000..af60902 --- /dev/null +++ b/BULK_DELETE_CONTRATS.md @@ -0,0 +1,65 @@ +# Suppression groupée de contrats - Page Staff/Contrats + +## Résumé des modifications + +Cette fonctionnalité permet au staff de supprimer plusieurs contrats en une seule opération depuis la page `/staff/contrats`. + +## Fichiers modifiés + +### 1. `components/staff/ContractsGrid.tsx` + +#### Ajouts : +- **État du modal** : `showDeleteModal` pour contrôler l'affichage du modal de confirmation +- **Bouton dans le menu actions groupées** : Ajout d'une option "Supprimer les contrats" en rouge dans le menu déroulant +- **Modal de confirmation** : `DeleteConfirmModal` - nouveau composant inline qui : + - Affiche un avertissement en rouge sur l'irréversibilité de l'action + - Liste les contrats qui seront supprimés + - Demande à l'utilisateur de taper "supprimer" pour confirmer + - Appelle l'API `/api/staff/contracts/bulk-delete` + - Gère les erreurs et affiche des notifications toast + +### 2. `app/api/staff/contracts/bulk-delete/route.ts` (nouveau fichier) + +Endpoint API POST qui : +- Vérifie l'authentification et les permissions staff +- Valide les IDs de contrats reçus +- Supprime les contrats de la table `cddu_contracts` +- Retourne la liste des IDs supprimés +- Gère les erreurs avec des messages appropriés + +## Fonctionnement + +1. L'utilisateur sélectionne un ou plusieurs contrats via les checkboxes +2. Clique sur "Action groupée" → "Supprimer les contrats" +3. Un modal s'ouvre avec : + - Un avertissement en rouge + - La liste des contrats à supprimer + - Un champ de confirmation où il faut taper "supprimer" +4. Une fois confirmé, les contrats sont supprimés de la base de données +5. La liste est automatiquement mise à jour et un toast de succès s'affiche +6. La sélection est réinitialisée + +## Sécurité + +- ✅ Vérification de l'authentification +- ✅ Vérification du statut staff +- ✅ Confirmation explicite requise (taper "supprimer") +- ✅ Avertissement visuel sur l'irréversibilité +- ✅ Séparation visuelle dans le menu (ligne de séparation + couleur rouge) + +## UX/UI + +- Bouton en rouge pour signaler le danger +- Modal avec fond rouge pour les avertissements +- Champ de confirmation obligatoire +- Toast de succès avec le nombre de contrats supprimés +- Mise à jour immédiate de la liste sans rechargement + +## Tests recommandés + +1. Tester avec 1 contrat +2. Tester avec plusieurs contrats +3. Tester l'annulation +4. Tester sans taper "supprimer" correctement +5. Vérifier les permissions (utilisateur non-staff) +6. Vérifier que les contrats sont bien supprimés de la BDD diff --git a/app/(app)/minima-ccn/ccnsvp/annexe5-data.tsx b/app/(app)/minima-ccn/ccnsvp/annexe5-data.tsx new file mode 100644 index 0000000..6c6d4ee --- /dev/null +++ b/app/(app)/minima-ccn/ccnsvp/annexe5-data.tsx @@ -0,0 +1,299 @@ +"use client"; + +import React, { useState } from 'react'; + +const euro = (n: number) => new Intl.NumberFormat('fr-FR', { + minimumFractionDigits: Number.isInteger(n) ? 0 : 2, + maximumFractionDigits: 2 +}).format(n) + '€'; + +interface Annexe5ContentProps {} + +export default function Annexe5Content({}: Annexe5ContentProps) { + const [activeSection, setActiveSection] = useState<'artistes' | 'techniciens'>('artistes'); + + return ( +
+ {/* En-tête */} +
+

+ Annexe 5 - Spectacles de cirque +

+

+ Producteurs ou diffuseurs de spectacles de cirque +

+

+ En application du titre VI des clauses communes et des articles 3.4, 3.5 et 4.3 de l'annexe 5 +

+
+ + {/* Navigation */} +
+ + +
+ + {/* Section Artistes-interprètes */} + {activeSection === 'artistes' && ( +
+ {/* Exploitation des spectacles */} +
+

+ + Artistes-interprètes du cirque et musiciens(nnes) - Exploitation des spectacles +

+ +
+ La grille des salaires concerne l'ensemble des contrats de travail : CDI, CDD, CDDU +
+ +
+ {/* En situation d'itinérance */} +
+

+ 🚐 En situation d'itinérance (spectacles sous chapiteau) +

+
+ + + + + + + + + + + + + + + + + +
TypeCachet1/2 cachet
(demi-représentation)
Mensuel
Rémunération{euro(117.23)}{euro(106.75)}{euro(1915.29)}
+
+
+ + {/* En tournée (hors chapiteau) */} +
+

+ 🎭 En tournée (hors chapiteau) +

+
+ + + + + + + + + + + + + + + + + +
TypeCachet1/2 cachet
(demi-représentation)
Mensuel
Rémunération{euro(128.59)}{euro(114.42)}{euro(1992.04)}
+
+
+
+
+ + {/* Répétitions / Création */} +
+

+ + Répétitions - Création +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
Type de cachetMontant
Cachet de base par jour{euro(106.75)}
Cachet de répétition en cas de service isolé pour les artistes de cirque{euro(62.42)}
Salaire mensuel{euro(1775.78)}
+
+ +
+ Note : La rémunération mensuelle est entendue pour 151,67 heures, pour un contrat d'une durée minimale d'un mois de date à date, sur une durée de 5 jours par semaine. +
+
+ + {/* Note informative */} +
+

+ 💡 Informations importantes +

+
+

+ Les minima salariaux présentés s'appliquent aux artistes-interprètes du cirque et aux musiciens(nnes) engagés dans le cadre de spectacles de cirque. +

+

+ Ces tarifs sont applicables que l'artiste soit en situation d'itinérance sous chapiteau ou en tournée dans des lieux fixes. +

+
+
+
+ )} + + {/* Section Personnel technique */} + {activeSection === 'techniciens' && ( +
+
+

+ + Grille de salaires minimaux - Personnel technique +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassificationMensuelTaux horaire
+ CADRES +
Cadres Groupe 1{euro(3547.66)}{euro(23.39)}
Cadres Groupe 2{euro(2923.27)}{euro(19.27)}
Cadres Groupe 3{euro(2287.32)}{euro(15.08)}
+ AGENTS DE MAÎTRISE +
Agents de maîtrise{euro(2142.14)}{euro(14.12)}
+ EMPLOYÉS QUALIFIÉS +
Employés qualifiés Groupe 1{euro(1911.36)}{euro(12.60)}
Employés qualifiés Groupe 2{euro(1812.13)}{euro(11.94)}
+ EMPLOYÉS +
Employés{euro(1777.90)}{euro(11.72)}
+
+ +
+

Note importante :

+

+ Les salaires mensuels sont calculés sur la base de 151,67 heures pour un contrat d'une durée minimale d'un mois de date à date, sur une durée de 5 jours par semaine. +

+
+
+ + {/* Détails des fonctions par catégorie */} +
+

+ 📋 Exemples de fonctions par catégorie +

+
+
+
Cadres Groupe 1
+
Directeur(trice) technique, régisseur(euse) général(e), directeur(trice) de production
+
+
+
Cadres Groupe 2
+
Concepteur(trice) lumière, concepteur(trice) du son, scénographe
+
+
+
Agents de maîtrise
+
Régisseur(euse), chef électricien(ne), chef machiniste, opérateur(trice) son
+
+
+
Employés qualifiés
+
Technicien(ne) son, technicien(ne) lumière, électricien(ne), machiniste, constructeur(trice)
+
+
+
+ 💡 Remarque : Pour la liste exhaustive des fonctions, se référer au titre VI des clauses communes de la convention collective. +
+
+
+ )} +
+ ); +} diff --git a/app/(app)/minima-ccn/ccnsvp/page.tsx b/app/(app)/minima-ccn/ccnsvp/page.tsx index 85941bf..5ed846e 100644 --- a/app/(app)/minima-ccn/ccnsvp/page.tsx +++ b/app/(app)/minima-ccn/ccnsvp/page.tsx @@ -9,6 +9,7 @@ import ClausesCommunesContent from './clauses-communes-data'; import Annexe2Content from './annexe2-data'; import Annexe3Content from './annexe3-data'; import Annexe4Content from './annexe4-data'; +import Annexe5Content from './annexe5-data'; export default function CCNSVPPage() { usePageTitle("Minima CCNSVP"); @@ -474,7 +475,7 @@ export default function CCNSVPPage() { aria-selected="false" aria-controls="ccnsvp-a5" tabIndex={-1} - data-tip="Producteurs, diffuseurs, organisateurs occasionnels (y compris les particuliers) de spectacles de bals avec ou sans orchestre" + data-tip="Producteurs ou diffuseurs de spectacles de cirque" > Annexe 5 @@ -483,7 +484,7 @@ export default function CCNSVPPage() { aria-selected="false" aria-controls="ccnsvp-a6" tabIndex={-1} - data-tip="Producteurs ou diffuseurs de spectacles de cirque" + data-tip="Producteurs, diffuseurs, organisateurs occasionnels (y compris les particuliers) de spectacles de bals avec ou sans orchestre" > Annexe 6 @@ -519,14 +520,12 @@ export default function CCNSVPPage() { diff --git a/app/api/staff/contracts/bulk-delete/route.ts b/app/api/staff/contracts/bulk-delete/route.ts new file mode 100644 index 0000000..3e84258 --- /dev/null +++ b/app/api/staff/contracts/bulk-delete/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import { createSbServer } from "@/lib/supabaseServer"; + +export async function POST(req: Request) { + try { + const sb = createSbServer(); + const { data: { user } } = await sb.auth.getUser(); + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const { data: me } = await sb.from("staff_users").select("is_staff").eq("user_id", user.id).maybeSingle(); + if (!me?.is_staff) return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + + const { contractIds } = await req.json(); + + if (!contractIds || !Array.isArray(contractIds) || contractIds.length === 0) { + return NextResponse.json({ error: "Contract IDs are required" }, { status: 400 }); + } + + // Supprimer tous les contrats sélectionnés + const { data: deletedContracts, error } = await sb + .from("cddu_contracts") + .delete() + .in("id", contractIds) + .select("id"); + + if (error) { + console.error("Error deleting contracts:", error); + return NextResponse.json({ error: "Failed to delete contracts" }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + deletedIds: deletedContracts?.map(c => c.id) || [], + message: `${deletedContracts?.length || 0} contrat(s) supprimé(s)` + }); + + } catch (err: any) { + console.error("Bulk delete error:", err); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/components/staff/ContractsGrid.tsx b/components/staff/ContractsGrid.tsx index fcf475b..900d7f9 100644 --- a/components/staff/ContractsGrid.tsx +++ b/components/staff/ContractsGrid.tsx @@ -171,6 +171,7 @@ export default function ContractsGrid({ initialData, activeOrgId }: { initialDat const [showEtatPaieModal, setShowEtatPaieModal] = useState(false); const [showSalaryModal, setShowSalaryModal] = useState(false); const [showActionMenu, setShowActionMenu] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); // PDF generation state const [isGeneratingPdfs, setIsGeneratingPdfs] = useState(false); @@ -1088,6 +1089,16 @@ export default function ContractsGrid({ initialData, activeOrgId }: { initialDat > Modifier État Paie +
+ @@ -1420,6 +1431,29 @@ export default function ContractsGrid({ initialData, activeOrgId }: { initialDat successCount={contractsESignProgress.filter(c => c.status === 'success').length} errorCount={contractsESignProgress.filter(c => c.status === 'error').length} /> + + {/* Modal de confirmation de suppression */} + {showDeleteModal && ( +
+
+

Confirmation de suppression

+

+ Êtes-vous sûr de vouloir supprimer {selectedContractIds.size} contrat{selectedContractIds.size > 1 ? 's' : ''} ? +

+ setShowDeleteModal(false)} + onSuccess={(deletedIds) => { + // Retirer les contrats supprimés de la liste + setRows(prev => prev.filter(row => !deletedIds.includes(row.id))); + setShowDeleteModal(false); + setSelectedContractIds(new Set()); + toast.success(`${deletedIds.length} contrat${deletedIds.length > 1 ? 's' : ''} supprimé${deletedIds.length > 1 ? 's' : ''}`); + }} + /> +
+
+ )} ); } @@ -1788,3 +1822,102 @@ function SalaryInputModal({ ); } + +// Modal pour confirmer la suppression des contrats +function DeleteConfirmModal({ + selectedContracts, + onClose, + onSuccess +}: { + selectedContracts: Contract[]; + onClose: () => void; + onSuccess: (deletedIds: string[]) => void; +}) { + const [loading, setLoading] = useState(false); + const [confirmText, setConfirmText] = useState(''); + + const handleSubmit = async () => { + if (confirmText.toLowerCase() !== 'supprimer') { + toast.error('Veuillez taper "supprimer" pour confirmer'); + return; + } + + setLoading(true); + try { + const response = await fetch('/api/staff/contracts/bulk-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contractIds: selectedContracts.map(c => c.id) + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Erreur lors de la suppression'); + } + + const result = await response.json(); + onSuccess(result.deletedIds); + } catch (error) { + console.error('Erreur:', error); + toast.error(error instanceof Error ? error.message : 'Erreur lors de la suppression des contrats'); + } finally { + setLoading(false); + } + }; + + return ( + <> +
+
+

+ ⚠️ Attention : Cette action est irréversible ! +

+

+ Les contrats suivants seront définitivement supprimés de la base de données. +

+
+ +
+

Contrats à supprimer :

+ {selectedContracts.map(contract => ( +
+ • {contract.contract_number || contract.id} - {formatEmployeeName(contract)} +
+ ))} +
+ +
+ + setConfirmText(e.target.value)} + placeholder="supprimer" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500" + /> +
+
+ +
+ + +
+ + ); +}