325 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|