espace-paie-odentas/components/DocumentPreviewModal.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

171 lines
5.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { X, Trash2, ExternalLink, Loader2 } from "lucide-react";
import { toast } from "sonner";
interface DocumentPreviewModalProps {
isOpen: boolean;
onClose: () => void;
document: {
key: string;
name: string;
type: string;
size: number;
downloadUrl: string;
} | null;
onDelete: (fileKey: string) => Promise<void>;
}
export function DocumentPreviewModal({
isOpen,
onClose,
document,
onDelete
}: DocumentPreviewModalProps) {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
if (!isOpen || !document) return null;
const isImage = document.name.match(/\.(jpg|jpeg|png)$/i);
const isPdf = document.name.match(/\.pdf$/i);
const handleDelete = async () => {
setIsDeleting(true);
try {
await onDelete(document.key);
toast.success("Document supprimé avec succès");
onClose();
} catch (error) {
toast.error("Erreur lors de la suppression du document");
console.error(error);
} finally {
setIsDeleting(false);
setShowDeleteConfirm(false);
}
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
<div className="flex-1 min-w-0">
<h2 className="text-lg font-semibold truncate">{document.name}</h2>
<div className="flex items-center gap-3 text-sm text-gray-500 mt-1">
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs font-medium">
{document.type}
</span>
<span>{formatFileSize(document.size)}</span>
</div>
</div>
<button
onClick={onClose}
className="ml-4 p-2 hover:bg-gray-200 rounded-lg transition-colors"
aria-label="Fermer"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-auto bg-gray-100 p-4">
{isImage && (
<div className="flex items-center justify-center h-full">
<img
src={document.downloadUrl}
alt={document.name}
className="max-w-full max-h-full object-contain rounded-lg shadow-lg"
/>
</div>
)}
{isPdf && (
<iframe
src={document.downloadUrl}
className="w-full h-full min-h-[600px] rounded-lg shadow-lg bg-white"
title={document.name}
/>
)}
{!isImage && !isPdf && (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-gray-500 mb-4">
Prévisualisation non disponible pour ce type de fichier
</p>
<a
href={document.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<ExternalLink className="h-4 w-4" />
Ouvrir dans un nouvel onglet / Télécharger
</a>
</div>
</div>
)}
</div>
{/* Footer with actions */}
<div className="flex items-center justify-between p-4 border-t bg-gray-50">
<a
href={document.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<ExternalLink className="h-4 w-4" />
Nouvel onglet / Télécharger
</a>
{!showDeleteConfirm ? (
<button
onClick={() => setShowDeleteConfirm(true)}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="h-4 w-4" />
Supprimer
</button>
) : (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 mr-2">Confirmer la suppression ?</span>
<button
onClick={() => setShowDeleteConfirm(false)}
className="px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-200 rounded-lg"
disabled={isDeleting}
>
Annuler
</button>
<button
onClick={handleDelete}
disabled={isDeleting}
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm bg-red-600 text-white hover:bg-red-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isDeleting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Suppression...
</>
) : (
<>
<Trash2 className="h-4 w-4" />
Confirmer
</>
)}
</button>
</div>
)}
</div>
</div>
</div>
);
}