espace-paie-odentas/components/RichTextEditor.tsx
odentas 73e914a303 feat: Système de statuts enrichi avec descriptions markdown et refonte navigation
- Header: Ajout 3ème ligne de statut (Caisses & orga.) avec descriptions détaillées
- Tooltips: Affichage riche avec titre, voyant coloré et contenu markdown formaté
- Éditeur markdown: Nouveau composant RichTextEditor avec toolbar (gras, italique, liens, listes)
- Modal staff: StatusEditModal étendu avec onglets et éditeur de descriptions
- Migration: Ajout colonnes status_*_description dans maintenance_status
- API: Routes GET/PUT enrichies pour gérer les 9 champs de statut
- Navigation: Redirection /compte/securite → /securite (nouvelle page centralisée)
- Breadcrumb: Support contrats RG/CDDU multi + labels dynamiques salariés
- UX Documents: Bouton 'Nouvel onglet / Télécharger' au lieu de téléchargement forcé
- Contrats staff: Pagination paies (6/page) pour RG et CDDU multi-mois avec vue compacte
- PayslipCard: Bouton cliquable 'Ouvrir le PDF' pour accès direct aux bulletins
2025-10-31 19:42:25 +01:00

168 lines
5.1 KiB
TypeScript

"use client";
import React, { useRef } from 'react';
import { Bold, Italic, List, Link as LinkIcon } from 'lucide-react';
interface RichTextEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
maxLength?: number;
}
export default function RichTextEditor({
value,
onChange,
placeholder = "Saisissez votre texte...",
maxLength = 1000
}: RichTextEditorProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const insertMarkdown = (before: string, after: string = '') => {
const textarea = textareaRef.current;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = value.substring(start, end);
const newText = value.substring(0, start) + before + selectedText + after + value.substring(end);
onChange(newText);
// Restaurer la sélection après le rendu
setTimeout(() => {
textarea.focus();
const newCursorPos = start + before.length + selectedText.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
};
const handleBold = () => insertMarkdown('**', '**');
const handleItalic = () => insertMarkdown('*', '*');
const handleList = () => {
const textarea = textareaRef.current;
if (!textarea) return;
const start = textarea.selectionStart;
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
const newText = value.substring(0, lineStart) + '• ' + value.substring(lineStart);
onChange(newText);
};
const handleLink = () => {
const url = prompt('Entrez l\'URL du lien :');
if (url) {
insertMarkdown('[', `](${url})`);
}
};
// Fonction pour prévisualiser le markdown en HTML simple
const renderPreview = (text: string) => {
let html = text;
// Gras
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Italique
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
// Liens
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" class="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">$1</a>');
// Listes - d'abord identifier les blocs de listes
const lines = html.split('\n');
let inList = false;
const processedLines: string[] = [];
lines.forEach((line) => {
if (line.trim().startsWith('• ')) {
if (!inList) {
processedLines.push('<ul class="list-disc list-inside space-y-0.5 my-2">');
inList = true;
}
processedLines.push(`<li class="ml-4">${line.trim().substring(2)}</li>`);
} else {
if (inList) {
processedLines.push('</ul>');
inList = false;
}
processedLines.push(line);
}
});
if (inList) {
processedLines.push('</ul>');
}
html = processedLines.join('\n');
// Retours à la ligne (sauf dans les listes)
html = html.replace(/\n(?![^\n]*<\/?(ul|li))/g, '<br />');
return html;
};
return (
<div className="space-y-2">
{/* Barre d'outils */}
<div className="flex items-center gap-1 p-2 bg-slate-50 border border-slate-300 rounded-t-lg">
<button
type="button"
onClick={handleBold}
className="p-2 hover:bg-slate-200 rounded transition-colors"
title="Gras (Ctrl+B)"
>
<Bold className="w-4 h-4 text-slate-700" />
</button>
<button
type="button"
onClick={handleItalic}
className="p-2 hover:bg-slate-200 rounded transition-colors"
title="Italique (Ctrl+I)"
>
<Italic className="w-4 h-4 text-slate-700" />
</button>
<button
type="button"
onClick={handleList}
className="p-2 hover:bg-slate-200 rounded transition-colors"
title="Liste à puces"
>
<List className="w-4 h-4 text-slate-700" />
</button>
<button
type="button"
onClick={handleLink}
className="p-2 hover:bg-slate-200 rounded transition-colors"
title="Insérer un lien"
>
<LinkIcon className="w-4 h-4 text-slate-700" />
</button>
<div className="ml-auto text-xs text-slate-500">
{value.length}/{maxLength} caractères
</div>
</div>
{/* Zone de texte */}
<textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
maxLength={maxLength}
className="w-full px-3 py-2 border border-slate-300 border-t-0 rounded-b-lg bg-white text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[120px] resize-y font-mono text-sm"
/>
{/* Prévisualisation */}
{value && (
<div className="p-3 bg-slate-50 border border-slate-300 rounded-lg">
<div className="text-xs font-medium text-slate-600 mb-2">Aperçu :</div>
<div
className="text-sm text-slate-700 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: renderPreview(value) }}
/>
</div>
)}
</div>
);
}