616 lines
19 KiB
TypeScript
616 lines
19 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import { usePageTitle } from '@/hooks/usePageTitle';
|
|
import Link from 'next/link';
|
|
import { ArrowLeft, Scale, Calculator, AlertTriangle, X } from 'lucide-react';
|
|
import Annexe1Content from './annexe1-data';
|
|
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';
|
|
import Annexe6Content from './annexe6-data';
|
|
import SimulateurContent from '@/components/simulateur/SimulateurContent';
|
|
import CalculatorComponent from '@/components/Calculator';
|
|
import { useDraggableModal } from '@/hooks/useDraggableModal';
|
|
|
|
export default function CCNSVPPage() {
|
|
usePageTitle("Minima CCNSVP");
|
|
const [isSimulateurOpen, setIsSimulateurOpen] = useState(false);
|
|
const [isCalculatorOpen, setIsCalculatorOpen] = useState(false);
|
|
const [modalPosition, setModalPosition] = useState({ x: 0, y: 0 });
|
|
const modalRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
// Etat de drag performant (hors React)
|
|
const dragState = React.useRef<{
|
|
dragging: boolean;
|
|
startX: number;
|
|
startY: number;
|
|
origLeft: number;
|
|
origTop: number;
|
|
dx: number;
|
|
dy: number;
|
|
raf: number | null;
|
|
iframe: HTMLIFrameElement | null;
|
|
}>({ dragging: false, startX: 0, startY: 0, origLeft: 0, origTop: 0, dx: 0, dy: 0, raf: null, iframe: null });
|
|
|
|
// Gestion du drag & drop du modal
|
|
const { onPointerDown, onPointerMove, onPointerUp } = useDraggableModal(
|
|
modalRef,
|
|
setModalPosition,
|
|
{ constrainToViewport: true, disableIframeDuringDrag: true }
|
|
);
|
|
|
|
// Écouter les messages de l'iframe pour ouvrir la calculatrice
|
|
useEffect(() => {
|
|
const handleMessage = (event: MessageEvent) => {
|
|
if (event.data?.type === 'openCalculator') {
|
|
setIsCalculatorOpen(true);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
return () => window.removeEventListener('message', handleMessage);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// Script de gestion des onglets
|
|
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
|
|
const panels = Array.from(document.querySelectorAll('.ccnsvp-panel'));
|
|
|
|
function activateTab(tab: Element) {
|
|
tabs.forEach((t) => {
|
|
const selected = t === tab;
|
|
t.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
(t as HTMLElement).tabIndex = selected ? 0 : -1;
|
|
});
|
|
|
|
panels.forEach((p) => {
|
|
const active = p.id === tab.getAttribute('aria-controls');
|
|
p.classList.toggle('active', active);
|
|
if (active) {
|
|
p.removeAttribute('hidden');
|
|
} else {
|
|
p.setAttribute('hidden', '');
|
|
}
|
|
});
|
|
|
|
(tab as HTMLElement).focus({ preventScroll: true });
|
|
}
|
|
|
|
tabs.forEach((tab) => {
|
|
tab.addEventListener('click', () => activateTab(tab));
|
|
});
|
|
|
|
// Navigation clavier
|
|
document.addEventListener('keydown', (e) => {
|
|
const current = document.querySelector('[role="tab"][aria-selected="true"]');
|
|
if (!current) return;
|
|
const i = tabs.indexOf(current);
|
|
if (e.key === 'ArrowRight') {
|
|
e.preventDefault();
|
|
activateTab(tabs[(i + 1) % tabs.length]);
|
|
}
|
|
if (e.key === 'ArrowLeft') {
|
|
e.preventDefault();
|
|
activateTab(tabs[(i - 1 + tabs.length) % tabs.length]);
|
|
}
|
|
if (e.key === 'Home') {
|
|
e.preventDefault();
|
|
activateTab(tabs[0]);
|
|
}
|
|
if (e.key === 'End') {
|
|
e.preventDefault();
|
|
activateTab(tabs[tabs.length - 1]);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<style jsx global>{`
|
|
.ccnsvp-tabs [role="tablist"] {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.ccnsvp-tabs [role="tab"] {
|
|
appearance: none;
|
|
border: 1px solid #e2e8f0;
|
|
background: white;
|
|
border-radius: 999px;
|
|
padding: 9px 16px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: #475569;
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
}
|
|
|
|
.ccnsvp-tabs [role="tab"]:hover {
|
|
background: #f8fafc;
|
|
border-color: #cbd5e1;
|
|
}
|
|
|
|
.ccnsvp-tabs [role="tab"][aria-selected="true"] {
|
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
border-color: #8b5cf6;
|
|
color: white;
|
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
|
}
|
|
|
|
.ccnsvp-tabs [role="tab"]:focus-visible {
|
|
outline: 2px solid #6366f1;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Tooltips */
|
|
.ccnsvp-tabs [role="tab"][data-tip]::after {
|
|
content: attr(data-tip);
|
|
position: absolute;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
top: calc(100% + 8px);
|
|
background: #0f172a;
|
|
color: white;
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
white-space: normal;
|
|
max-width: min(72vw, 340px);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
z-index: 50;
|
|
transition: opacity 0.2s;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.ccnsvp-tabs [role="tab"][data-tip]::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
top: calc(100% + 4px);
|
|
border: 6px solid transparent;
|
|
border-bottom-color: #0f172a;
|
|
opacity: 0;
|
|
z-index: 50;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.ccnsvp-tabs [role="tab"][data-tip]:hover::after,
|
|
.ccnsvp-tabs [role="tab"][data-tip]:hover::before,
|
|
.ccnsvp-tabs [role="tab"][data-tip]:focus-visible::after,
|
|
.ccnsvp-tabs [role="tab"][data-tip]:focus-visible::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.ccnsvp-panel {
|
|
display: none;
|
|
}
|
|
|
|
.ccnsvp-panel.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Alertes */
|
|
.ccnsvp-alert {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-radius: 12px;
|
|
border: 1px solid #fed7aa;
|
|
background: #fff7ed;
|
|
color: #9a3412;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.ccnsvp-alert .icon {
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Tables */
|
|
.ccnsvp-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.ccnsvp-table th,
|
|
.ccnsvp-table td {
|
|
border-top: 1px solid #e2e8f0;
|
|
padding: 12px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.ccnsvp-table th {
|
|
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
|
|
font-weight: 700;
|
|
color: #475569;
|
|
border-top-color: #c7d2fe;
|
|
}
|
|
|
|
.ccnsvp-table tbody tr:first-child td {
|
|
border-top: 0;
|
|
}
|
|
|
|
.ccnsvp-table tbody tr:hover {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
/* KPIs */
|
|
.ccnsvp-kpi {
|
|
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
|
|
border: 1px solid #c7d2fe;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.ccnsvp-kpi .lbl {
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
color: #64748b;
|
|
margin: 0 0 6px 0;
|
|
}
|
|
|
|
.ccnsvp-kpi .num {
|
|
font-size: 24px;
|
|
font-weight: 900;
|
|
color: #1e293b;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.ccnsvp-kpi .muted {
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Recherche */
|
|
.ccnsvp-search {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.ccnsvp-search input {
|
|
flex: 1;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.ccnsvp-search input:focus {
|
|
outline: 2px solid #6366f1;
|
|
outline-offset: 0;
|
|
border-color: #6366f1;
|
|
}
|
|
|
|
.ccnsvp-badge {
|
|
display: inline-block;
|
|
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
|
|
border: 1px solid #c7d2fe;
|
|
border-radius: 999px;
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #4f46e5;
|
|
}
|
|
|
|
/* Accordéons */
|
|
.ccnsvp-acc details {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
background: white;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.ccnsvp-acc summary {
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
outline: none;
|
|
list-style: none;
|
|
}
|
|
|
|
.ccnsvp-acc summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.ccnsvp-acc ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 12px 0 0;
|
|
}
|
|
|
|
.ccnsvp-acc li {
|
|
padding: 10px 0;
|
|
border-top: 1px solid #e2e8f0;
|
|
cursor: pointer;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.ccnsvp-acc li:first-child {
|
|
border-top: 0;
|
|
}
|
|
|
|
.ccnsvp-acc li:hover {
|
|
color: #6366f1;
|
|
}
|
|
|
|
/* Highlight animation */
|
|
.hl {
|
|
animation: flash 2s ease;
|
|
}
|
|
|
|
@keyframes flash {
|
|
0% { background: #fff7ed; }
|
|
100% { background: transparent; }
|
|
}
|
|
|
|
/* Grille responsive */
|
|
.ccnsvp-grid {
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
@media (min-width: 900px) {
|
|
.ccnsvp-grid {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
/* KPI blocks */
|
|
.kpi-blocks {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
gap: 12px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.kpi-block {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 10px;
|
|
padding: 12px;
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.kpi-block-label {
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
color: #475569;
|
|
}
|
|
|
|
.kpi-block-num {
|
|
font-weight: 900;
|
|
font-size: 18px;
|
|
color: #1e293b;
|
|
white-space: nowrap;
|
|
}
|
|
`}</style>
|
|
|
|
{/* Navigation retour */}
|
|
<Link
|
|
href="/minima-ccn"
|
|
className="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900 transition-colors"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Retour aux minima CCN
|
|
</Link>
|
|
|
|
{/* En-tête */}
|
|
<section className="rounded-2xl border bg-white p-6">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
|
<Scale className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<h1 className="text-2xl font-semibold text-slate-900">CCNSVP (IDCC 3090)</h1>
|
|
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-gradient-to-r from-emerald-500 to-emerald-600 text-white shadow-sm">
|
|
À jour 2025
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-slate-600 mb-2">
|
|
Spectacle vivant privé - Minima techniciens, artistes et personnels administratifs et commerciaux
|
|
</p>
|
|
<p className="text-xs text-slate-500">
|
|
Données issues de l'accord sur les salaires signé le 25/01/2024, entré en vigueur le 01/02/2024, étendu par arrêté ministériel du 25/03/2024.
|
|
<br />
|
|
Toujours en vigueur au 2e semestre 2025. <strong>Tous les montants sont exprimés bruts.</strong>
|
|
</p>
|
|
<div className="ccnsvp-alert mt-3">
|
|
<AlertTriangle className="w-5 h-5 flex-shrink-0" aria-hidden="true" />
|
|
<span>
|
|
Les <b>metteurs en scène</b> ne sont pas prévus par la CCNSVP ;
|
|
il convient donc d'appliquer au minimum le <b>SMIC</b> (taux horaire de 11,88€ brut à ce jour).
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Onglets */}
|
|
<section className="rounded-2xl border bg-white p-6 ccnsvp-tabs">
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Survolez le nom d'une annexe pour connaître son titre exact, cliquez sur l'annexe pour accéder aux détails.
|
|
</p>
|
|
|
|
<div role="tablist" aria-label="Onglets CCNSVP">
|
|
<button
|
|
role="tab"
|
|
aria-selected="true"
|
|
aria-controls="ccnsvp-communes"
|
|
tabIndex={0}
|
|
data-tip="Grilles de salaires des emplois administratifs et commerciaux, et des emplois techniques hors annexe"
|
|
>
|
|
Clauses communes
|
|
</button>
|
|
<button
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="ccnsvp-a1"
|
|
tabIndex={-1}
|
|
data-tip="Annexe 1 — Exploitants de lieux, producteurs ou diffuseurs de spectacles dramatiques, lyriques, chorégraphiques et de musique classique"
|
|
>
|
|
Annexe 1
|
|
</button>
|
|
<button
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="ccnsvp-a2"
|
|
tabIndex={-1}
|
|
data-tip="Annexe 2 — Exploitants de lieux, producteurs ou diffuseurs de spectacles de chanson, variétés, jazz, musiques actuelles"
|
|
>
|
|
Annexe 2
|
|
</button>
|
|
<button
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="ccnsvp-a3"
|
|
tabIndex={-1}
|
|
data-tip="Exploitants de lieux, producteurs ou diffuseurs de spectacles de cabarets"
|
|
>
|
|
Annexe 3
|
|
</button>
|
|
<button
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="ccnsvp-a4"
|
|
tabIndex={-1}
|
|
data-tip="Producteurs ou diffuseurs de spectacles en tournée (à l'exception des cirques et des bals)"
|
|
>
|
|
Annexe 4
|
|
</button>
|
|
<button
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="ccnsvp-a5"
|
|
tabIndex={-1}
|
|
data-tip="Producteurs ou diffuseurs de spectacles de cirque"
|
|
>
|
|
Annexe 5
|
|
</button>
|
|
<button
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="ccnsvp-a6"
|
|
tabIndex={-1}
|
|
data-tip="Producteurs, diffuseurs, organisateurs occasionnels (y compris les particuliers) de spectacles de bals avec ou sans orchestre"
|
|
>
|
|
Annexe 6
|
|
</button>
|
|
</div>
|
|
|
|
{/* Panneaux */}
|
|
<div id="ccnsvp-communes" className="ccnsvp-panel active" role="tabpanel">
|
|
<ClausesCommunesContent />
|
|
</div>
|
|
|
|
<div id="ccnsvp-a1" className="ccnsvp-panel" role="tabpanel" hidden>
|
|
<Annexe1Content />
|
|
</div>
|
|
|
|
<div id="ccnsvp-a2" className="ccnsvp-panel" role="tabpanel" hidden>
|
|
<Annexe2Content />
|
|
</div>
|
|
|
|
<div id="ccnsvp-a3" className="ccnsvp-panel" role="tabpanel" hidden>
|
|
<Annexe3Content />
|
|
</div>
|
|
|
|
<div id="ccnsvp-a4" className="ccnsvp-panel" role="tabpanel" hidden>
|
|
<Annexe4Content />
|
|
</div>
|
|
|
|
<div id="ccnsvp-a5" className="ccnsvp-panel" role="tabpanel" hidden>
|
|
<Annexe5Content />
|
|
</div>
|
|
|
|
<div id="ccnsvp-a6" className="ccnsvp-panel" role="tabpanel" hidden>
|
|
<Annexe6Content />
|
|
</div>
|
|
</section>
|
|
|
|
{/* Bouton flottant Simulateur */}
|
|
<button
|
|
onClick={() => setIsSimulateurOpen(!isSimulateurOpen)}
|
|
className="fixed bottom-6 right-6 z-50 inline-flex items-center gap-2 px-5 py-3 rounded-full text-sm font-bold bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 hover:from-amber-500 hover:to-amber-600 transition-all shadow-lg hover:shadow-xl hover:scale-105"
|
|
>
|
|
<Calculator className="w-5 h-5" />
|
|
Simulateur
|
|
</button>
|
|
|
|
{/* Modale compacte qui sort du bouton */}
|
|
{isSimulateurOpen && (
|
|
<>
|
|
{/* Modale compacte déplaçable */}
|
|
<div
|
|
ref={modalRef}
|
|
className="fixed z-50 w-[500px] max-h-[680px] bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
|
|
style={{
|
|
left: modalPosition.x !== 0 ? `${modalPosition.x}px` : 'auto',
|
|
top: modalPosition.y !== 0 ? `${modalPosition.y}px` : 'auto',
|
|
bottom: modalPosition.x === 0 && modalPosition.y === 0 ? '6rem' : 'auto',
|
|
right: modalPosition.x === 0 && modalPosition.y === 0 ? '1.5rem' : 'auto',
|
|
}}
|
|
>
|
|
{/* Header draggable */}
|
|
<div
|
|
className="flex items-center justify-between px-4 py-3 border-b bg-gradient-to-r from-amber-50 to-orange-50 cursor-move select-none touch-none"
|
|
onPointerDown={onPointerDown}
|
|
onPointerMove={onPointerMove}
|
|
onPointerUp={onPointerUp}
|
|
>
|
|
<h3 className="text-sm font-bold text-slate-900 flex items-center gap-2">
|
|
<Calculator className="w-4 h-4 text-amber-600" />
|
|
Simulateur de paie
|
|
</h3>
|
|
<button
|
|
onClick={() => {
|
|
setIsSimulateurOpen(false);
|
|
setModalPosition({ x: 0, y: 0 }); // Réinitialiser la position
|
|
}}
|
|
className="p-1.5 rounded-lg hover:bg-slate-100 transition-colors"
|
|
aria-label="Fermer le simulateur"
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
>
|
|
<X className="w-4 h-4 text-slate-600" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content avec scroll */}
|
|
<div className="flex-1 overflow-auto p-4">
|
|
<SimulateurContent hideInfoPanel />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Calculatrice globale */}
|
|
<CalculatorComponent
|
|
isOpen={isCalculatorOpen}
|
|
onClose={() => setIsCalculatorOpen(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|