espace-paie-odentas/components/staff/DocumentViewerModal.tsx

325 lines
12 KiB
TypeScript

"use client";
import { useState } from "react";
import { X, Trash2, Edit3, Download, Loader2, ExternalLink } from "lucide-react";
import { toast } from "sonner";
interface DocumentViewerModalProps {
isOpen: boolean;
onClose: () => void;
document: {
key: string;
name: string;
type: string;
size: number;
lastModified: string;
downloadUrl: string;
} | null;
onDocumentUpdated: () => void;
}
const DOCUMENT_TYPES = [
{ value: "piece_identite", label: "Pièce d'identité" },
{ value: "attestation_secu", label: "Attestation Sécurité Sociale" },
{ value: "rib", label: "RIB" },
{ value: "medecine_travail", label: "Attestation médecine du travail" },
{ value: "contrat_travail", label: "Contrat de travail" },
{ value: "diplome", label: "Diplôme" },
{ value: "justificatif", label: "Justificatif" },
{ value: "autre", label: "Autre document" },
];
// Fonction pour deviner le type à partir du label
const guessTypeFromLabel = (label: string): string => {
const normalizedLabel = label.toLowerCase();
if (normalizedLabel.includes("pièce") || normalizedLabel.includes("identité")) return "piece_identite";
if (normalizedLabel.includes("attestation") && normalizedLabel.includes("sécu")) return "attestation_secu";
if (normalizedLabel.includes("rib")) return "rib";
if (normalizedLabel.includes("médecine") || normalizedLabel.includes("travail")) return "medecine_travail";
if (normalizedLabel.includes("contrat")) return "contrat_travail";
if (normalizedLabel.includes("diplôme") || normalizedLabel.includes("diplome")) return "diplome";
if (normalizedLabel.includes("justificatif")) return "justificatif";
return "autre";
};
export default function DocumentViewerModal({
isOpen,
onClose,
document,
onDocumentUpdated,
}: DocumentViewerModalProps) {
const [isEditingType, setIsEditingType] = useState(false);
const [selectedType, setSelectedType] = useState("");
const [isUpdating, setIsUpdating] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
if (!isOpen || !document) return null;
// Déterminer si c'est un PDF ou une image
const isPDF = document.downloadUrl.toLowerCase().includes('.pdf');
const isImage = /\.(jpg|jpeg|png)$/i.test(document.downloadUrl);
const currentType = guessTypeFromLabel(document.type);
const handleUpdateType = async () => {
if (!selectedType || selectedType === currentType) {
setIsEditingType(false);
return;
}
setIsUpdating(true);
try {
const response = await fetch('/api/staff/salaries/documents/update-type', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: document.key,
newType: selectedType,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Erreur lors de la mise à jour');
}
toast.success('Type de document mis à jour');
setIsEditingType(false);
onDocumentUpdated();
onClose();
} catch (error) {
console.error('Erreur mise à jour type:', error);
toast.error(error instanceof Error ? error.message : 'Erreur lors de la mise à jour');
} finally {
setIsUpdating(false);
}
};
const handleDelete = async () => {
setIsDeleting(true);
try {
const response = await fetch(`/api/staff/salaries/documents/delete?key=${encodeURIComponent(document.key)}`, {
method: 'DELETE',
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Erreur lors de la suppression');
}
toast.success('Document supprimé avec succès');
onDocumentUpdated();
onClose();
} catch (error) {
console.error('Erreur suppression:', error);
toast.error(error instanceof Error ? error.message : 'Erreur lors de la suppression');
} finally {
setIsDeleting(false);
setShowDeleteConfirm(false);
}
};
const handleStartEdit = () => {
setSelectedType(currentType);
setIsEditingType(true);
};
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} o`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} Ko`;
return `${(bytes / (1024 * 1024)).toFixed(1)} Mo`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="relative w-full max-w-5xl max-h-[90vh] bg-white rounded-2xl shadow-2xl flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-5 border-b bg-slate-50 rounded-t-2xl">
<div className="flex-1 min-w-0 mr-4">
{isEditingType ? (
<div className="flex items-center gap-2">
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
disabled={isUpdating}
className="px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
>
{DOCUMENT_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<button
onClick={handleUpdateType}
disabled={isUpdating}
className="px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1"
>
{isUpdating ? (
<>
<Loader2 className="size-4 animate-spin" />
Mise à jour...
</>
) : (
'Valider'
)}
</button>
<button
onClick={() => setIsEditingType(false)}
disabled={isUpdating}
className="px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-200 rounded-lg transition-colors disabled:opacity-50"
>
Annuler
</button>
</div>
) : (
<>
<h2 className="text-lg font-semibold text-slate-800 truncate">
{document.type}
</h2>
<p className="text-sm text-slate-600">
{formatSize(document.size)} {formatDate(document.lastModified)}
</p>
</>
)}
</div>
<div className="flex items-center gap-2">
{!isEditingType && (
<>
<button
onClick={handleStartEdit}
className="p-2 hover:bg-slate-200 rounded-lg transition-colors"
title="Modifier le type"
>
<Edit3 className="size-5 text-slate-600" />
</button>
<a
href={document.downloadUrl}
download
className="p-2 hover:bg-slate-200 rounded-lg transition-colors"
title="Télécharger"
>
<Download className="size-5 text-slate-600" />
</a>
<a
href={document.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="p-2 hover:bg-slate-200 rounded-lg transition-colors"
title="Ouvrir dans un nouvel onglet"
>
<ExternalLink className="size-5 text-slate-600" />
</a>
<button
onClick={() => setShowDeleteConfirm(true)}
className="p-2 hover:bg-red-100 rounded-lg transition-colors"
title="Supprimer"
>
<Trash2 className="size-5 text-red-600" />
</button>
</>
)}
<button
onClick={onClose}
disabled={isUpdating || isDeleting}
className="p-2 hover:bg-slate-200 rounded-lg transition-colors disabled:opacity-50"
>
<X className="size-5 text-slate-500" />
</button>
</div>
</div>
{/* Body - Viewer */}
<div className="flex-1 overflow-auto bg-slate-100 p-4">
{isPDF ? (
<iframe
src={document.downloadUrl}
className="w-full h-full min-h-[500px] rounded-lg bg-white shadow"
title="Document PDF"
/>
) : isImage ? (
<div className="flex items-center justify-center h-full">
<img
src={document.downloadUrl}
alt={document.type}
className="max-w-full max-h-full object-contain rounded-lg shadow-lg"
/>
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-slate-600 mb-4">
Prévisualisation non disponible pour ce type de fichier
</p>
<a
href={document.downloadUrl}
download
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
<Download className="size-4" />
Télécharger le fichier
</a>
</div>
</div>
)}
</div>
{/* Confirmation de suppression */}
{showDeleteConfirm && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-2xl">
<div className="bg-white rounded-xl p-6 max-w-md mx-4 shadow-2xl">
<h3 className="text-lg font-semibold text-slate-800 mb-2">
Confirmer la suppression
</h3>
<p className="text-sm text-slate-600 mb-6">
Êtes-vous sûr de vouloir supprimer ce document ? Cette action est irréversible.
</p>
<div className="flex items-center justify-end gap-3">
<button
onClick={() => setShowDeleteConfirm(false)}
disabled={isDeleting}
className="px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg transition-colors disabled:opacity-50"
>
Annuler
</button>
<button
onClick={handleDelete}
disabled={isDeleting}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isDeleting ? (
<>
<Loader2 className="size-4 animate-spin" />
Suppression...
</>
) : (
<>
<Trash2 className="size-4" />
Supprimer
</>
)}
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}