2813 lines
114 KiB
TypeScript
2813 lines
114 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { useSearchParams, useRouter } from "next/navigation"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import {
|
|
FileText,
|
|
Grid3X3,
|
|
List,
|
|
Search,
|
|
Filter,
|
|
Plus,
|
|
Upload,
|
|
Download,
|
|
Eye,
|
|
Edit,
|
|
Trash2,
|
|
FolderOpen,
|
|
Star,
|
|
Clock,
|
|
FileImage,
|
|
FileSpreadsheet,
|
|
FileVideo,
|
|
Archive,
|
|
SortAsc,
|
|
SortDesc,
|
|
X,
|
|
CheckCircle,
|
|
XCircle,
|
|
Info,
|
|
UserPlus,
|
|
FileQuestion,
|
|
ShieldCheck,
|
|
Cloud,
|
|
HardDrive,
|
|
CloudUpload,
|
|
Users,
|
|
Crown,
|
|
Shield,
|
|
User,
|
|
Folder,
|
|
Brain,
|
|
Settings,
|
|
Calendar,
|
|
AlertTriangle,
|
|
} from "lucide-react"
|
|
|
|
interface Document {
|
|
id: number
|
|
name: string
|
|
type: string
|
|
size: string
|
|
modified: Date
|
|
created: Date
|
|
author: string
|
|
folder: string
|
|
folderId: string
|
|
tags: string[]
|
|
status: string
|
|
thumbnail: string
|
|
description?: string
|
|
version: string
|
|
isValidated: boolean
|
|
hasCertificate: boolean
|
|
summary?: string
|
|
storageType: "temporary" | "permanent"
|
|
permissions: {
|
|
canView: boolean
|
|
canEdit: boolean
|
|
canDelete: boolean
|
|
canInvite: boolean
|
|
canValidate: boolean
|
|
canArchive: boolean
|
|
canAnalyze: boolean
|
|
}
|
|
temporaryStorageConfig?: {
|
|
duration: number // en jours
|
|
dataUsage: string
|
|
thirdPartyAccess: string
|
|
}
|
|
}
|
|
|
|
interface ActionModal {
|
|
type:
|
|
| "view"
|
|
| "edit"
|
|
| "invite"
|
|
| "delete"
|
|
| "move"
|
|
| "rename"
|
|
| "request"
|
|
| "validate"
|
|
| "certificate"
|
|
| "archive"
|
|
| "storage_config"
|
|
| "import"
|
|
| "new_document"
|
|
| null
|
|
document: Document | null
|
|
documents: Document[]
|
|
}
|
|
|
|
interface UserWithRoles {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
avatar: string
|
|
folderRoles: {
|
|
[folderId: string]: {
|
|
role: "owner" | "editor" | "viewer" | "validator" | "contributor"
|
|
assignedDate: Date
|
|
}
|
|
}
|
|
spaceRole: "admin" | "manager" | "user" | "guest"
|
|
spaceRoles: {
|
|
[spaceId: string]: {
|
|
role: "admin" | "manager" | "user" | "guest"
|
|
spaceName: string
|
|
}
|
|
}
|
|
}
|
|
|
|
interface Role {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
level: "folder" | "space" | "global"
|
|
}
|
|
|
|
export default function DocumentsPage() {
|
|
const searchParams = useSearchParams()
|
|
const router = useRouter()
|
|
const folderFilter = searchParams.get("folder")
|
|
|
|
const [viewMode, setViewMode] = useState<'list'>('list')
|
|
const [searchTerm, setSearchTerm] = useState("")
|
|
const [selectedDocuments, setSelectedDocuments] = useState<number[]>([])
|
|
const [sortBy, setSortBy] = useState("modified")
|
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
|
|
const [filterType, setFilterType] = useState("all")
|
|
const [filterAuthor, setFilterAuthor] = useState("all")
|
|
const [filterStorage, setFilterStorage] = useState("all")
|
|
const [filterFolder, setFilterFolder] = useState(folderFilter || "all")
|
|
const [showFilters, setShowFilters] = useState(false)
|
|
const [actionModal, setActionModal] = useState<ActionModal>({ type: null, document: null, documents: [] })
|
|
|
|
// Modal states
|
|
const [inviteMessage, setInviteMessage] = useState("")
|
|
const [selectedUser, setSelectedUser] = useState("")
|
|
const [selectedRole, setSelectedRole] = useState("")
|
|
const [inviteScope, setInviteScope] = useState<"user" | "role">("user")
|
|
const [newFolderName, setNewFolderName] = useState("")
|
|
const [newDocumentName, setNewDocumentName] = useState("")
|
|
const [editDescription, setEditDescription] = useState("")
|
|
const [editAttachment, setEditAttachment] = useState<File | null>(null)
|
|
const [editMessage, setEditMessage] = useState("")
|
|
const [requestDocumentName, setRequestDocumentName] = useState("")
|
|
const [requestMessage, setRequestMessage] = useState("")
|
|
const [archiveReason, setArchiveReason] = useState("")
|
|
const [retentionPeriod, setRetentionPeriod] = useState("5")
|
|
const [notification, setNotification] = useState<{ type: "success" | "error" | "info"; message: string } | null>(null)
|
|
|
|
// Storage configuration states
|
|
const [storageDuration, setStorageDuration] = useState("30")
|
|
const [dataUsage, setDataUsage] = useState("")
|
|
const [thirdPartyAccess, setThirdPartyAccess] = useState("")
|
|
|
|
// Import states
|
|
const [importFiles, setImportFiles] = useState<FileList | null>(null)
|
|
const [importFolder, setImportFolder] = useState("")
|
|
const [importDescription, setImportDescription] = useState("")
|
|
|
|
const [documents, setDocuments] = useState<Document[]>([])
|
|
const [stats, setStats] = useState({
|
|
total: 0,
|
|
thisWeek: 0,
|
|
validated: 0,
|
|
favorites: 0,
|
|
permanent: 0,
|
|
temporary: 0,
|
|
})
|
|
|
|
const [folders] = useState([
|
|
{ id: "general", name: "Général" },
|
|
])
|
|
|
|
const [users] = useState<UserWithRoles[]>([
|
|
{
|
|
id: "1",
|
|
name: "Marie Dubois",
|
|
email: "marie.dubois@company.com",
|
|
avatar: "MD",
|
|
folderRoles: {
|
|
general: { role: "owner", assignedDate: new Date("2024-01-01") },
|
|
},
|
|
spaceRole: "manager",
|
|
spaceRoles: {
|
|
main: { role: "manager", spaceName: "Espace Principal" },
|
|
legal: { role: "admin", spaceName: "Espace Juridique" },
|
|
},
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "Sophie Laurent",
|
|
email: "sophie.laurent@company.com",
|
|
avatar: "SL",
|
|
folderRoles: {
|
|
general: { role: "editor", assignedDate: new Date("2024-01-02") },
|
|
},
|
|
spaceRole: "user",
|
|
spaceRoles: {
|
|
main: { role: "user", spaceName: "Espace Principal" },
|
|
analytics: { role: "manager", spaceName: "Espace Analytics" },
|
|
},
|
|
},
|
|
{
|
|
id: "3",
|
|
name: "Jean Martin",
|
|
email: "jean.martin@company.com",
|
|
avatar: "JM",
|
|
folderRoles: {
|
|
general: { role: "viewer", assignedDate: new Date("2024-01-03") },
|
|
},
|
|
spaceRole: "user",
|
|
spaceRoles: {
|
|
main: { role: "user", spaceName: "Espace Principal" },
|
|
projects: { role: "admin", spaceName: "Espace Projets" },
|
|
},
|
|
},
|
|
{
|
|
id: "4",
|
|
name: "Pierre Durand",
|
|
email: "pierre.durand@company.com",
|
|
avatar: "PD",
|
|
folderRoles: {
|
|
general: { role: "contributor", assignedDate: new Date("2024-01-04") },
|
|
},
|
|
spaceRole: "user",
|
|
spaceRoles: {
|
|
main: { role: "user", spaceName: "Espace Principal" },
|
|
training: { role: "admin", spaceName: "Espace Formation" },
|
|
},
|
|
},
|
|
{
|
|
id: "5",
|
|
name: "Admin Système",
|
|
email: "admin@company.com",
|
|
avatar: "AD",
|
|
folderRoles: {
|
|
policies: { role: "owner", assignedDate: new Date("2024-01-01") },
|
|
archives: { role: "owner", assignedDate: new Date("2024-01-01") },
|
|
contracts: { role: "validator", assignedDate: new Date("2024-01-01") },
|
|
finance: { role: "validator", assignedDate: new Date("2024-01-01") },
|
|
},
|
|
spaceRole: "admin",
|
|
spaceRoles: {
|
|
main: { role: "admin", spaceName: "Espace Principal" },
|
|
legal: { role: "admin", spaceName: "Espace Juridique" },
|
|
analytics: { role: "admin", spaceName: "Espace Analytics" },
|
|
projects: { role: "admin", spaceName: "Espace Projets" },
|
|
training: { role: "admin", spaceName: "Espace Formation" },
|
|
},
|
|
},
|
|
])
|
|
|
|
const [roles] = useState<Role[]>([
|
|
{
|
|
id: "folder-owner",
|
|
name: "Propriétaire du dossier",
|
|
description: "Contrôle total sur le dossier",
|
|
level: "folder",
|
|
},
|
|
{ id: "folder-editor", name: "Éditeur du dossier", description: "Peut modifier les documents", level: "folder" },
|
|
{
|
|
id: "folder-validator",
|
|
name: "Validateur du dossier",
|
|
description: "Peut valider les documents",
|
|
level: "folder",
|
|
},
|
|
{
|
|
id: "folder-contributor",
|
|
name: "Contributeur du dossier",
|
|
description: "Peut ajouter des documents",
|
|
level: "folder",
|
|
},
|
|
{ id: "folder-viewer", name: "Lecteur du dossier", description: "Lecture seule", level: "folder" },
|
|
{ id: "space-admin", name: "Administrateur d'espace", description: "Contrôle total sur l'espace", level: "space" },
|
|
{
|
|
id: "space-manager",
|
|
name: "Gestionnaire d'espace",
|
|
description: "Gestion des utilisateurs et dossiers",
|
|
level: "space",
|
|
},
|
|
{ id: "space-user", name: "Utilisateur d'espace", description: "Accès standard à l'espace", level: "space" },
|
|
{ id: "space-guest", name: "Invité d'espace", description: "Accès limité à l'espace", level: "space" },
|
|
{ id: "global-admin", name: "Administrateur global", description: "Accès à tous les espaces", level: "global" },
|
|
{ id: "global-manager", name: "Gestionnaire global", description: "Gestion multi-espaces", level: "global" },
|
|
])
|
|
|
|
// Mettre à jour le filtre de dossier quand l'URL change
|
|
useEffect(() => {
|
|
if (folderFilter) {
|
|
setFilterFolder(folderFilter)
|
|
setShowFilters(true)
|
|
}
|
|
}, [folderFilter])
|
|
|
|
useEffect(() => {
|
|
// Simuler le chargement des documents
|
|
const loadDocuments = () => {
|
|
const mockDocuments: Document[] = [
|
|
{
|
|
id: 1,
|
|
name: "Contrat_Client_ABC.pdf",
|
|
type: "PDF",
|
|
size: "2.4 MB",
|
|
modified: new Date("2024-01-15T10:30:00"),
|
|
created: new Date("2024-01-15T09:00:00"),
|
|
author: "Marie Dubois",
|
|
folder: "Contrats",
|
|
folderId: "contracts",
|
|
tags: ["contrat", "client", "juridique"],
|
|
status: "validated",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=PDF",
|
|
description: "Contrat de prestation de services avec le client ABC Corp.",
|
|
version: "v1.2",
|
|
isValidated: true,
|
|
hasCertificate: true,
|
|
storageType: "permanent",
|
|
summary:
|
|
"Contrat de prestation de services d'une durée de 12 mois avec ABC Corp. Montant total : 150 000€ HT. Clauses de confidentialité et de propriété intellectuelle incluses.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: true,
|
|
canArchive: false,
|
|
canAnalyze: true,
|
|
},
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Rapport_Mensuel_Nov.docx",
|
|
type: "DOCX",
|
|
size: "1.8 MB",
|
|
modified: new Date("2024-01-15T08:45:00"),
|
|
created: new Date("2024-01-10T14:20:00"),
|
|
author: "Sophie Laurent",
|
|
folder: "Rapports",
|
|
folderId: "reports",
|
|
tags: ["rapport", "mensuel", "analyse"],
|
|
status: "pending",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=DOCX",
|
|
description: "Rapport mensuel d'activité pour novembre 2024.",
|
|
version: "v2.1",
|
|
isValidated: false,
|
|
hasCertificate: false,
|
|
storageType: "temporary",
|
|
summary:
|
|
"Rapport d'activité mensuel présentant les KPIs, les réalisations et les objectifs pour le mois de novembre. Croissance de 15% par rapport au mois précédent.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
canInvite: true,
|
|
canValidate: false,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
temporaryStorageConfig: {
|
|
duration: 90,
|
|
dataUsage: "Rapport mensuel d'activité pour suivi des performances",
|
|
thirdPartyAccess: "Équipe direction, consultants externes autorisés",
|
|
},
|
|
},
|
|
{
|
|
id: 3,
|
|
name: "Présentation_Projet.pptx",
|
|
type: "PPTX",
|
|
size: "5.2 MB",
|
|
modified: new Date("2024-01-15T07:15:00"),
|
|
created: new Date("2024-01-12T11:30:00"),
|
|
author: "Jean Martin",
|
|
folder: "Projets",
|
|
folderId: "projects",
|
|
tags: ["présentation", "projet", "alpha"],
|
|
status: "draft",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=PPTX",
|
|
description: "Présentation du projet Alpha pour le comité de direction.",
|
|
version: "v1.0",
|
|
isValidated: false,
|
|
hasCertificate: false,
|
|
storageType: "temporary",
|
|
summary:
|
|
"Présentation détaillée du projet Alpha incluant le planning, le budget prévisionnel de 500K€ et les ressources nécessaires. Lancement prévu en Q2 2024.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: true,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
temporaryStorageConfig: {
|
|
duration: 180,
|
|
dataUsage: "Présentation projet pour validation comité de direction",
|
|
thirdPartyAccess: "Comité de direction, équipe projet, partenaires techniques",
|
|
},
|
|
},
|
|
{
|
|
id: 4,
|
|
name: "Budget_2024.xlsx",
|
|
type: "XLSX",
|
|
size: "892 KB",
|
|
modified: new Date("2024-01-14T16:20:00"),
|
|
created: new Date("2024-01-08T09:45:00"),
|
|
author: "Marie Dubois",
|
|
folder: "Finance",
|
|
folderId: "finance",
|
|
tags: ["budget", "2024", "finance"],
|
|
status: "validated",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=XLSX",
|
|
description: "Budget prévisionnel pour l'année 2024.",
|
|
version: "v3.0",
|
|
isValidated: true,
|
|
hasCertificate: true,
|
|
storageType: "permanent",
|
|
summary:
|
|
"Budget prévisionnel 2024 avec une allocation totale de 2.5M€. Répartition par département et projets stratégiques. Croissance prévue de 20%.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: true,
|
|
canArchive: false,
|
|
canAnalyze: true,
|
|
},
|
|
},
|
|
{
|
|
id: 5,
|
|
name: "Politique_Sécurité.pdf",
|
|
type: "PDF",
|
|
size: "1.1 MB",
|
|
modified: new Date("2024-01-14T14:10:00"),
|
|
created: new Date("2024-01-05T10:00:00"),
|
|
author: "Admin Système",
|
|
folder: "Politiques",
|
|
folderId: "policies",
|
|
tags: ["sécurité", "politique", "règlement"],
|
|
status: "validated",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=PDF",
|
|
description: "Politique de sécurité informatique de l'entreprise.",
|
|
version: "v1.0",
|
|
isValidated: true,
|
|
hasCertificate: true,
|
|
storageType: "permanent",
|
|
summary:
|
|
"Document officiel définissant les règles de sécurité informatique. Couvre la gestion des mots de passe, l'accès aux données et les procédures d'incident.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
canInvite: false,
|
|
canValidate: false,
|
|
canArchive: false,
|
|
canAnalyze: true,
|
|
},
|
|
},
|
|
{
|
|
id: 6,
|
|
name: "Formation_Équipe.mp4",
|
|
type: "MP4",
|
|
size: "45.2 MB",
|
|
modified: new Date("2024-01-13T11:30:00"),
|
|
created: new Date("2024-01-13T11:30:00"),
|
|
author: "Pierre Durand",
|
|
folder: "Formation",
|
|
folderId: "training",
|
|
tags: ["formation", "vidéo", "équipe"],
|
|
status: "draft",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=MP4",
|
|
description: "Vidéo de formation pour la nouvelle équipe.",
|
|
version: "v1.0",
|
|
isValidated: false,
|
|
hasCertificate: false,
|
|
storageType: "temporary",
|
|
summary:
|
|
"Vidéo de formation de 45 minutes couvrant les processus internes, les outils utilisés et les bonnes pratiques. Destinée aux nouveaux collaborateurs.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: true,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
temporaryStorageConfig: {
|
|
duration: 365,
|
|
dataUsage: "Vidéo de formation pour onboarding nouveaux collaborateurs",
|
|
thirdPartyAccess: "RH, managers, nouveaux employés, prestataires formation",
|
|
},
|
|
},
|
|
{
|
|
id: 7,
|
|
name: "Logo_Entreprise.png",
|
|
type: "PNG",
|
|
size: "256 KB",
|
|
modified: new Date("2024-01-12T15:45:00"),
|
|
created: new Date("2024-01-12T15:45:00"),
|
|
author: "Design Team",
|
|
folder: "Assets",
|
|
folderId: "assets",
|
|
tags: ["logo", "design", "branding"],
|
|
status: "validated",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=PNG",
|
|
description: "Logo officiel de l'entreprise en haute résolution.",
|
|
version: "v2.0",
|
|
isValidated: true,
|
|
hasCertificate: false,
|
|
storageType: "temporary",
|
|
summary:
|
|
"Logo officiel de l'entreprise en format PNG haute résolution (300 DPI). Versions couleur et monochrome disponibles pour tous supports de communication.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: true,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
temporaryStorageConfig: {
|
|
duration: 730,
|
|
dataUsage: "Logo officiel pour supports de communication et marketing",
|
|
thirdPartyAccess: "Équipe marketing, agences externes, partenaires commerciaux",
|
|
},
|
|
},
|
|
{
|
|
id: 8,
|
|
name: "Archive_2023.zip",
|
|
type: "ZIP",
|
|
size: "128 MB",
|
|
modified: new Date("2024-01-10T09:00:00"),
|
|
created: new Date("2024-01-10T09:00:00"),
|
|
author: "Admin Système",
|
|
folder: "Archives",
|
|
folderId: "archives",
|
|
tags: ["archive", "2023", "backup"],
|
|
status: "archived",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=ZIP",
|
|
description: "Archive complète des documents de l'année 2023.",
|
|
version: "v1.0",
|
|
isValidated: true,
|
|
hasCertificate: false,
|
|
storageType: "permanent",
|
|
summary:
|
|
"Archive complète contenant tous les documents de l'année 2023. Inclut les contrats, rapports, présentations et documents administratifs.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
canInvite: false,
|
|
canValidate: false,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
},
|
|
{
|
|
id: 9,
|
|
name: "Note_Projet_Nouveau.docx",
|
|
type: "DOCX",
|
|
size: "0.8 MB",
|
|
modified: new Date(),
|
|
created: new Date(),
|
|
author: "Utilisateur actuel",
|
|
folder: "Nouveaux Dossiers",
|
|
folderId: "new-folders",
|
|
tags: ["nouveau", "projet"],
|
|
status: "draft",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=DOCX",
|
|
description: "Documentation initiale d'un nouveau projet.",
|
|
version: "v0.1",
|
|
isValidated: false,
|
|
hasCertificate: false,
|
|
storageType: "temporary",
|
|
summary: "Note de cadrage et premiers éléments du projet.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: false,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
},
|
|
{
|
|
id: 10,
|
|
name: "Budget_Preliminaire.xlsx",
|
|
type: "XLSX",
|
|
size: "0.3 MB",
|
|
modified: new Date(),
|
|
created: new Date(),
|
|
author: "Utilisateur actuel",
|
|
folder: "Lancements",
|
|
folderId: "launch",
|
|
tags: ["nouveau", "budget"],
|
|
status: "pending",
|
|
thumbnail: "/placeholder.svg?height=120&width=120&text=XLSX",
|
|
description: "Budget préliminaire pour un nouveau dossier.",
|
|
version: "v0.1",
|
|
isValidated: false,
|
|
hasCertificate: false,
|
|
storageType: "temporary",
|
|
summary: "Prévisions de coûts et enveloppe initiale.",
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: false,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
},
|
|
]
|
|
|
|
setDocuments(mockDocuments)
|
|
setStats({
|
|
total: mockDocuments.length,
|
|
thisWeek: mockDocuments.filter((doc) => {
|
|
const weekAgo = new Date()
|
|
weekAgo.setDate(weekAgo.getDate() - 7)
|
|
return doc.modified > weekAgo
|
|
}).length,
|
|
validated: mockDocuments.filter((doc) => doc.isValidated).length,
|
|
favorites: mockDocuments.filter((doc) => doc.favorite).length,
|
|
permanent: mockDocuments.filter((doc) => doc.storageType === "permanent").length,
|
|
temporary: mockDocuments.filter((doc) => doc.storageType === "temporary").length,
|
|
})
|
|
}
|
|
|
|
loadDocuments()
|
|
}, [])
|
|
|
|
// Notification system
|
|
const showNotification = (type: "success" | "error" | "info", message: string) => {
|
|
setNotification({ type, message })
|
|
setTimeout(() => setNotification(null), 3000)
|
|
}
|
|
|
|
// Fonction pour envoyer une notification dans le chat du dossier
|
|
const sendFolderChatNotification = (folderId: string, message: string, actionType: string, documentName?: string) => {
|
|
const folderUsers = users.filter((user) => user.folderRoles[folderId])
|
|
|
|
console.log("Notification envoyée dans le chat du dossier:", {
|
|
folderId,
|
|
folderName: folders.find((f) => f.id === folderId)?.name,
|
|
recipients: folderUsers.map((u) => ({ name: u.name, role: u.folderRoles[folderId]?.role })),
|
|
message,
|
|
actionType,
|
|
documentName,
|
|
timestamp: new Date().toISOString(),
|
|
})
|
|
|
|
folderUsers.forEach((user) => {
|
|
console.log(`📱 Notification push envoyée à ${user.name} (${user.email})`)
|
|
})
|
|
}
|
|
|
|
// Document actions
|
|
const handleViewDocument = (doc: Document) => {
|
|
setActionModal({ type: "view", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleEditDocument = (doc: Document) => {
|
|
setEditDescription(doc.description || "")
|
|
setEditMessage("")
|
|
setEditAttachment(null)
|
|
setActionModal({ type: "edit", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleInviteDocument = (doc: Document) => {
|
|
setInviteMessage("")
|
|
setSelectedUser("")
|
|
setSelectedRole("")
|
|
setInviteScope("user")
|
|
setActionModal({ type: "invite", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleDeleteDocument = (doc: Document) => {
|
|
setActionModal({ type: "delete", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleMoveDocument = (doc: Document) => {
|
|
setNewFolderName(doc.folder)
|
|
setActionModal({ type: "move", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleRenameDocument = (doc: Document) => {
|
|
setNewDocumentName(doc.name)
|
|
setActionModal({ type: "rename", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleDownloadDocument = (doc: Document) => {
|
|
const storageText = doc.storageType === "permanent" ? "stockage permanent" : "stockage temporaire"
|
|
showNotification("info", `Récupération de ${doc.name} du ${storageText}...`)
|
|
|
|
sendFolderChatNotification(doc.folderId, `📥 ${doc.name} a été récupéré du ${storageText}`, "download", doc.name)
|
|
|
|
setTimeout(() => {
|
|
showNotification("success", `${doc.name} récupéré avec succès`)
|
|
}, 1500)
|
|
}
|
|
|
|
const handleDownloadCertificate = (doc: Document) => {
|
|
if (doc.hasCertificate) {
|
|
showNotification("info", `Téléchargement du certificat blockchain pour ${doc.name}...`)
|
|
|
|
sendFolderChatNotification(
|
|
doc.folderId,
|
|
`🔗 Certificat blockchain téléchargé pour ${doc.name}`,
|
|
"blockchain_certificate_download",
|
|
doc.name,
|
|
)
|
|
|
|
setTimeout(() => {
|
|
showNotification("success", `Certificat blockchain de ${doc.name} téléchargé avec succès`)
|
|
}, 2000)
|
|
}
|
|
}
|
|
|
|
const handleArchiveDocument = (doc: Document) => {
|
|
setArchiveReason("")
|
|
setRetentionPeriod("5")
|
|
setActionModal({ type: "archive", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleRequestDocument = (selectedDocs: Document[]) => {
|
|
setRequestDocumentName("")
|
|
setRequestMessage("")
|
|
setActionModal({ type: "request", document: null, documents: selectedDocs })
|
|
}
|
|
|
|
const handleImportDocuments = () => {
|
|
setImportFiles(null)
|
|
setImportFolder(filterFolder !== "all" ? filterFolder : "")
|
|
setImportDescription("")
|
|
setActionModal({ type: "import", document: null, documents: [] })
|
|
}
|
|
|
|
const handleValidateDocuments = (docIds: number[]) => {
|
|
if (!docIds || docIds.length === 0) return;
|
|
// Ouvre le modal de validation pour le premier document sélectionné (ou gérer en bulk si besoin)
|
|
const doc = documents.find((d) => d.id === docIds[0]);
|
|
if (doc) {
|
|
setActionModal({ type: "validate", document: doc, documents: docIds.map(id => documents.find(d => d.id === id)).filter(Boolean) });
|
|
}
|
|
};
|
|
|
|
const handleViewCertificate = (doc: Document) => {
|
|
if (doc.hasCertificate) {
|
|
setActionModal({ type: "certificate", document: doc, documents: [] })
|
|
}
|
|
}
|
|
|
|
const handleConfigureStorage = (doc: Document) => {
|
|
if (doc.temporaryStorageConfig) {
|
|
setStorageDuration(doc.temporaryStorageConfig.duration.toString())
|
|
setDataUsage(doc.temporaryStorageConfig.dataUsage)
|
|
setThirdPartyAccess(doc.temporaryStorageConfig.thirdPartyAccess)
|
|
} else {
|
|
setStorageDuration("30")
|
|
setDataUsage("")
|
|
setThirdPartyAccess("")
|
|
}
|
|
setActionModal({ type: "storage_config", document: doc, documents: [] })
|
|
}
|
|
|
|
const handleToggleFavorite = (docId: number) => {
|
|
const doc = documents.find((d) => d.id === docId)
|
|
if (!doc) return
|
|
|
|
setDocuments((prev) => prev.map((d) => (d.id === docId ? { ...d, favorite: !d.favorite } : d)))
|
|
|
|
const action = doc.favorite ? "retiré des" : "ajouté aux"
|
|
showNotification("success", `${doc.name} ${action} favoris`)
|
|
|
|
sendFolderChatNotification(doc.folderId, `⭐ ${doc.name} a été ${action} favoris`, "favorite", doc.name)
|
|
}
|
|
|
|
const handleAIAnalysis = (doc: Document) => {
|
|
showNotification("info", `Analyse IA en cours pour ${doc.name}...`)
|
|
|
|
setTimeout(
|
|
() => {
|
|
const analysisResults = [
|
|
// Analyse pour PDF/Contrats
|
|
`📄 **Analyse IA du document "${doc.name}"**\n\n` +
|
|
`**Type de document :** ${doc.type} (${doc.size})\n` +
|
|
`**Statut :** ${doc.isValidated ? "✅ Validé" : "⏳ En attente"}\n` +
|
|
`**Dernière modification :** ${formatDate(doc.modified)}\n\n` +
|
|
`**Analyse du contenu :**\n` +
|
|
`• ${doc.type === "PDF" ? "Document juridique détecté" : "Document standard"}\n` +
|
|
`• ${doc.tags.length} tag(s) identifié(s) : ${doc.tags.join(", ")}\n` +
|
|
`• ${doc.summary ? "Résumé automatique disponible" : "Contenu analysé"}\n\n` +
|
|
`**Métriques de qualité :**\n` +
|
|
`• Lisibilité : ${Math.floor(Math.random() * 20) + 80}%\n` +
|
|
`• Conformité : ${doc.isValidated ? "100%" : Math.floor(Math.random() * 30) + 60 + "%"}\n` +
|
|
`• Sécurité : ${doc.storageType === "permanent" ? "Maximale" : "Standard"}\n\n` +
|
|
`**Recommandations :**\n` +
|
|
`• ${doc.storageType === "temporary" ? "Archivage permanent recommandé" : "Archivage optimal"}\n` +
|
|
`• ${!doc.isValidated ? "Validation requise avant finalisation" : "Document prêt pour utilisation"}\n` +
|
|
`• ${doc.tags.length < 3 ? "Améliorer le tagging pour une meilleure recherche" : "Tagging satisfaisant"}\n\n` +
|
|
`**Score global :** ${Math.floor(Math.random() * 20) + 80}/100`,
|
|
|
|
// Analyse spécialisée par type
|
|
`🔍 **Analyse spécialisée - ${doc.name}**\n\n` +
|
|
`**Classification automatique :**\n` +
|
|
`• Format : ${doc.type} (${doc.size})\n` +
|
|
`• Catégorie : ${doc.folder}\n` +
|
|
`• Complexité : ${["Faible", "Modérée", "Élevée"][Math.floor(Math.random() * 3)]}\n\n` +
|
|
`**Analyse technique :**\n` +
|
|
`${
|
|
doc.type === "PDF"
|
|
? "• Pages analysées : " +
|
|
Math.floor(Math.random() * 50 + 10) +
|
|
"\n• Clauses détectées : " +
|
|
Math.floor(Math.random() * 15 + 5)
|
|
: doc.type === "XLSX"
|
|
? "• Feuilles analysées : " +
|
|
Math.floor(Math.random() * 10 + 1) +
|
|
"\n• Formules détectées : " +
|
|
Math.floor(Math.random() * 100 + 20)
|
|
: doc.type === "DOCX"
|
|
? "• Mots analysés : " +
|
|
Math.floor(Math.random() * 5000 + 1000) +
|
|
"\n• Sections détectées : " +
|
|
Math.floor(Math.random() * 10 + 3)
|
|
: "• Contenu multimédia analysé\n• Métadonnées extraites"
|
|
}\n\n` +
|
|
`**Insights IA :**\n` +
|
|
`• ${Math.random() > 0.5 ? "Document fréquemment consulté" : "Usage modéré détecté"}\n` +
|
|
`• ${Math.random() > 0.5 ? "Collaboration active identifiée" : "Document principalement individuel"}\n` +
|
|
`• ${doc.version !== "v1.0" ? "Historique de versions riche" : "Document récent"}\n\n` +
|
|
`**Actions suggérées :**\n` +
|
|
`• ${Math.random() > 0.5 ? "Créer un modèle basé sur ce document" : "Standardiser le format"}\n` +
|
|
`• ${Math.random() > 0.5 ? "Planifier une révision" : "Maintenir la version actuelle"}\n` +
|
|
`• Prochaine analyse : ${new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString("fr-FR")}`,
|
|
|
|
// Analyse de conformité et sécurité
|
|
`🛡️ **Audit de conformité - ${doc.name}**\n\n` +
|
|
`**Analyse de sécurité :**\n` +
|
|
`• Chiffrement : ${doc.storageType === "permanent" ? "AES-256" : "Standard"}\n` +
|
|
`• Accès : ${doc.permissions.canView ? "Contrôlé" : "Restreint"}\n` +
|
|
`• Traçabilité : ${doc.hasCertificate ? "Complète" : "Partielle"}\n\n` +
|
|
`**Conformité RGPD :**\n` +
|
|
`• Données personnelles : ${Math.random() > 0.7 ? "⚠️ Détectées" : "✅ Aucune"}\n` +
|
|
`• Durée de conservation : ${doc.storageType === "permanent" ? "Conforme" : "À vérifier"}\n` +
|
|
`• Droit à l'oubli : ${Math.random() > 0.5 ? "Applicable" : "Non applicable"}\n\n` +
|
|
`**Analyse des risques :**\n` +
|
|
`• Niveau de risque : ${["Faible", "Modéré", "Élevé"][Math.floor(Math.random() * 3)]}\n` +
|
|
`• Exposition : ${doc.permissions.canInvite ? "Partageable" : "Interne uniquement"}\n` +
|
|
`• Criticité : ${doc.isValidated ? "Validée" : "À évaluer"}\n\n` +
|
|
`**Recommandations de sécurité :**\n` +
|
|
`• ${doc.storageType === "temporary" ? "Migration vers stockage sécurisé recommandée" : "Sécurité optimale"}\n` +
|
|
`• ${!doc.hasCertificate ? "Certification numérique suggérée" : "Certification à jour"}\n` +
|
|
`• Audit de sécurité : ${Math.random() > 0.5 ? "Recommandé dans 6 mois" : "Conforme pour 12 mois"}\n\n` +
|
|
`**Score de conformité :** ${Math.floor(Math.random() * 15) + 85}/100`,
|
|
]
|
|
|
|
const randomAnalysis = analysisResults[Math.floor(Math.random() * analysisResults.length)]
|
|
|
|
// Envoyer l'analyse dans le chat du dossier
|
|
sendFolderChatNotification(doc.folderId, `🤖 ${randomAnalysis}`, "document_ai_analysis", doc.name)
|
|
|
|
showNotification("success", `Analyse IA terminée pour ${doc.name}. Redirection vers le chat...`)
|
|
|
|
// Rediriger vers le chat après 1.5 secondes
|
|
setTimeout(() => {
|
|
router.push("/dashboard/chat")
|
|
}, 1500)
|
|
},
|
|
2000 + Math.random() * 2000,
|
|
)
|
|
}
|
|
|
|
const handleManageDocumentRoles = (doc: Document) => {
|
|
// Rediriger vers la gestion des rôles du document
|
|
router.push(`/dashboard/documents/${doc.id}/roles`)
|
|
}
|
|
|
|
// Bulk actions
|
|
const handleBulkDownload = () => {
|
|
const selectedDocs = documents.filter((doc) => selectedDocuments.includes(doc.id))
|
|
showNotification("info", `Récupération de ${selectedDocs.length} document(s)...`)
|
|
|
|
const folderGroups = selectedDocs.reduce(
|
|
(acc, doc) => {
|
|
if (!acc[doc.folderId]) acc[doc.folderId] = []
|
|
acc[doc.folderId].push(doc.name)
|
|
return acc
|
|
},
|
|
{} as { [folderId: string]: string[] },
|
|
)
|
|
|
|
Object.entries(folderGroups).forEach(([folderId, docNames]) => {
|
|
sendFolderChatNotification(
|
|
folderId,
|
|
`📥 ${docNames.length} document(s) récupéré(s) : ${docNames.join(", ")}`,
|
|
"bulk_download",
|
|
)
|
|
})
|
|
|
|
setTimeout(() => {
|
|
showNotification("success", `${selectedDocs.length} document(s) récupéré(s) avec succès`)
|
|
setSelectedDocuments([])
|
|
}, 2000)
|
|
}
|
|
|
|
const handleBulkInvite = () => {
|
|
const selectedDocs = documents.filter((doc) => selectedDocuments.includes(doc.id) && doc.permissions.canInvite)
|
|
if (selectedDocs.length === 0) {
|
|
showNotification("error", "Aucun document sélectionné ne peut être partagé")
|
|
return
|
|
}
|
|
setInviteMessage("")
|
|
setSelectedUser("")
|
|
setSelectedRole("")
|
|
setInviteScope("user")
|
|
setActionModal({ type: "invite", document: null, documents: selectedDocs })
|
|
}
|
|
|
|
const handleBulkArchive = () => {
|
|
const selectedDocs = documents.filter((doc) => selectedDocuments.includes(doc.id) && doc.permissions.canArchive)
|
|
if (selectedDocs.length === 0) {
|
|
showNotification("error", "Aucun document sélectionné ne peut être archivé")
|
|
return
|
|
}
|
|
setArchiveReason("")
|
|
setRetentionPeriod("5")
|
|
setActionModal({ type: "archive", document: null, documents: selectedDocs })
|
|
}
|
|
|
|
const handleBulkMove = () => {
|
|
const selectedDocs = documents.filter((doc) => selectedDocuments.includes(doc.id))
|
|
setNewFolderName("")
|
|
setActionModal({ type: "move", document: null, documents: selectedDocs })
|
|
}
|
|
|
|
const handleBulkDelete = () => {
|
|
const selectedDocs = documents.filter((doc) => selectedDocuments.includes(doc.id))
|
|
setActionModal({ type: "delete", document: null, documents: selectedDocs })
|
|
}
|
|
|
|
const handleBulkAIAnalysis = () => {
|
|
const selectedDocs = documents.filter((doc) => selectedDocuments.includes(doc.id) && doc.permissions.canAnalyze)
|
|
if (selectedDocs.length === 0) {
|
|
showNotification("error", "Aucun document sélectionné ne peut être analysé")
|
|
return
|
|
}
|
|
|
|
showNotification("info", `Analyse IA en cours pour ${selectedDocs.length} document(s)...`)
|
|
|
|
// Analyser chaque document avec un délai échelonné
|
|
selectedDocs.forEach((doc, index) => {
|
|
setTimeout(() => {
|
|
const bulkAnalysis =
|
|
`📊 **Analyse IA groupée - Document "${doc.name}"**\n\n` +
|
|
`**Position dans l'analyse :** ${index + 1}/${selectedDocs.length}\n` +
|
|
`**Type :** ${doc.type} (${doc.size})\n` +
|
|
`**Dossier :** ${doc.folder}\n\n` +
|
|
`**Analyse rapide :**\n` +
|
|
`• Statut : ${doc.isValidated ? "✅ Validé" : "⏳ En attente"}\n` +
|
|
`• Stockage : ${doc.storageType === "permanent" ? "☁️ Permanent" : "💾 Temporaire"}\n` +
|
|
`• Tags : ${doc.tags.join(", ")}\n\n` +
|
|
`**Score de qualité :** ${Math.floor(Math.random() * 20) + 75}/100\n` +
|
|
`**Recommandation :** ${doc.storageType === "temporary" ? "Archivage suggéré" : "Optimisé"}`
|
|
|
|
sendFolderChatNotification(doc.folderId, `🤖 ${bulkAnalysis}`, "bulk_document_ai_analysis", doc.name)
|
|
}, index * 1000)
|
|
})
|
|
|
|
setTimeout(
|
|
() => {
|
|
const totalSize = selectedDocs.reduce((sum, doc) => sum + Number.parseFloat(doc.size.replace(/[^\d.]/g, "")), 0)
|
|
showNotification(
|
|
"success",
|
|
`Analyse IA terminée pour ${selectedDocs.length} document(s) (${totalSize.toFixed(1)} MB). Redirection vers le chat...`,
|
|
)
|
|
setSelectedDocuments([])
|
|
|
|
// Rediriger vers le chat après l'analyse groupée
|
|
setTimeout(() => {
|
|
router.push("/dashboard/chat")
|
|
}, 1500)
|
|
},
|
|
selectedDocs.length * 1000 + 1000,
|
|
)
|
|
}
|
|
|
|
// Modal actions
|
|
const confirmInvite = () => {
|
|
const recipient =
|
|
inviteScope === "user"
|
|
? users.find((u) => u.id === selectedUser)?.name
|
|
: roles.find((r) => r.id === selectedRole)?.name
|
|
|
|
if (actionModal.document) {
|
|
showNotification("success", `${actionModal.document.name} partagé avec ${recipient}. Un message a été envoyé.`)
|
|
|
|
sendFolderChatNotification(
|
|
actionModal.document.folderId,
|
|
`👥 ${actionModal.document.name} a été partagé avec ${recipient}. Message: ${inviteMessage}`,
|
|
"invite",
|
|
actionModal.document.name,
|
|
)
|
|
} else if (actionModal.documents.length > 0) {
|
|
const folderGroups = actionModal.documents.reduce(
|
|
(acc, doc) => {
|
|
if (!acc[doc.folderId]) acc[doc.folderId] = []
|
|
acc[doc.folderId].push(doc.name)
|
|
return acc
|
|
},
|
|
{} as { [folderId: string]: string[] },
|
|
)
|
|
|
|
Object.entries(folderGroups).forEach(([folderId, docNames]) => {
|
|
sendFolderChatNotification(
|
|
folderId,
|
|
`👥 ${docNames.length} document(s) partagé(s) avec ${recipient} : ${docNames.join(", ")}. Message: ${inviteMessage}`,
|
|
"bulk_invite",
|
|
)
|
|
})
|
|
|
|
showNotification(
|
|
"success",
|
|
`${actionModal.documents.length} document(s) partagé(s) avec ${recipient}. Messages envoyés.`,
|
|
)
|
|
setSelectedDocuments([])
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmDelete = () => {
|
|
if (actionModal.document) {
|
|
sendFolderChatNotification(
|
|
actionModal.document.folderId,
|
|
`🗑️ ${actionModal.document.name} a été supprimé`,
|
|
"delete",
|
|
actionModal.document.name,
|
|
)
|
|
|
|
setDocuments((prev) => prev.filter((doc) => doc.id !== actionModal.document!.id))
|
|
showNotification("success", `${actionModal.document.name} supprimé`)
|
|
} else if (actionModal.documents.length > 0) {
|
|
const folderGroups = actionModal.documents.reduce(
|
|
(acc, doc) => {
|
|
if (!acc[doc.folderId]) acc[doc.folderId] = []
|
|
acc[doc.folderId].push(doc.name)
|
|
return acc
|
|
},
|
|
{} as { [folderId: string]: string[] },
|
|
)
|
|
|
|
Object.entries(folderGroups).forEach(([folderId, docNames]) => {
|
|
sendFolderChatNotification(
|
|
folderId,
|
|
`🗑️ ${docNames.length} document(s) supprimé(s) : ${docNames.join(", ")}`,
|
|
"bulk_delete",
|
|
)
|
|
})
|
|
|
|
const docIds = actionModal.documents.map((doc) => doc.id)
|
|
setDocuments((prev) => prev.filter((doc) => !docIds.includes(doc.id)))
|
|
showNotification("success", `${actionModal.documents.length} document(s) supprimé(s)`)
|
|
setSelectedDocuments([])
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmMove = () => {
|
|
if (actionModal.document) {
|
|
const oldFolderId = actionModal.document.folderId
|
|
const newFolder = folders.find((f) => f.name === newFolderName)
|
|
const newFolderId = newFolder?.id || oldFolderId
|
|
|
|
const updatedDoc = { ...actionModal.document, folder: newFolderName, folderId: newFolderId }
|
|
setDocuments((prev) => prev.map((doc) => (doc.id === updatedDoc.id ? updatedDoc : doc)))
|
|
showNotification("success", `${actionModal.document.name} déplacé vers ${newFolderName}`)
|
|
|
|
if (oldFolderId !== newFolderId) {
|
|
sendFolderChatNotification(
|
|
oldFolderId,
|
|
`📤 ${actionModal.document.name} a été déplacé vers ${newFolderName}`,
|
|
"move_out",
|
|
actionModal.document.name,
|
|
)
|
|
sendFolderChatNotification(
|
|
newFolderId,
|
|
`📥 ${actionModal.document.name} a été déplacé depuis ${actionModal.document.folder}`,
|
|
"move_in",
|
|
actionModal.document.name,
|
|
)
|
|
}
|
|
} else if (actionModal.documents.length > 0) {
|
|
const newFolder = folders.find((f) => f.name === newFolderName)
|
|
const newFolderId = newFolder?.id || ""
|
|
|
|
const folderGroups = actionModal.documents.reduce(
|
|
(acc, doc) => {
|
|
if (!acc[doc.folderId]) acc[doc.folderId] = []
|
|
acc[doc.folderId].push(doc.name)
|
|
return acc
|
|
},
|
|
{} as { [folderId: string]: string[] },
|
|
)
|
|
|
|
Object.entries(folderGroups).forEach(([oldFolderId, docNames]) => {
|
|
if (oldFolderId !== newFolderId) {
|
|
sendFolderChatNotification(
|
|
oldFolderId,
|
|
`📤 ${docNames.length} document(s) déplacé(s) vers ${newFolderName} : ${docNames.join(", ")}`,
|
|
"bulk_move_out",
|
|
)
|
|
}
|
|
})
|
|
|
|
if (newFolderId) {
|
|
sendFolderChatNotification(
|
|
newFolderId,
|
|
`📥 ${actionModal.documents.length} document(s) déplacé(s) dans ce dossier : ${actionModal.documents.map((d) => d.name).join(", ")}`,
|
|
"bulk_move_in",
|
|
)
|
|
}
|
|
|
|
const docIds = actionModal.documents.map((doc) => doc.id)
|
|
setDocuments((prev) =>
|
|
prev.map((doc) => (docIds.includes(doc.id) ? { ...doc, folder: newFolderName, folderId: newFolderId } : doc)),
|
|
)
|
|
showNotification("success", `${actionModal.documents.length} document(s) déplacé(s) vers ${newFolderName}`)
|
|
setSelectedDocuments([])
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmRename = () => {
|
|
if (actionModal.document) {
|
|
const updatedDoc = { ...actionModal.document, name: newDocumentName }
|
|
setDocuments((prev) => prev.map((doc) => (doc.id === updatedDoc.id ? updatedDoc : doc)))
|
|
showNotification("success", `Document renommé en ${newDocumentName}`)
|
|
|
|
sendFolderChatNotification(
|
|
actionModal.document.folderId,
|
|
`✏️ Document renommé : "${actionModal.document.name}" → "${newDocumentName}"`,
|
|
"rename",
|
|
newDocumentName,
|
|
)
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmEdit = () => {
|
|
if (actionModal.document) {
|
|
const updatedDoc = {
|
|
...actionModal.document,
|
|
description: editDescription,
|
|
modified: new Date(),
|
|
}
|
|
setDocuments((prev) => prev.map((doc) => (doc.id === updatedDoc.id ? updatedDoc : doc)))
|
|
showNotification("success", `${actionModal.document.name} mis à jour`)
|
|
|
|
let message = `✏️ ${actionModal.document.name} a été modifié`
|
|
if (editMessage.trim()) {
|
|
message += ` - ${editMessage}`
|
|
}
|
|
if (editAttachment) {
|
|
message += ` (avec pièce jointe: ${editAttachment.name})`
|
|
}
|
|
|
|
sendFolderChatNotification(actionModal.document.folderId, message, "edit", actionModal.document.name)
|
|
|
|
showNotification("info", "Message envoyé dans le chat du dossier")
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmRequest = () => {
|
|
if (!requestDocumentName.trim()) {
|
|
showNotification("error", "Veuillez spécifier le nom du document à demander")
|
|
return
|
|
}
|
|
if (!selectedRole) {
|
|
showNotification("error", "Veuillez sélectionner le rôle de destination")
|
|
return
|
|
}
|
|
|
|
const requestData = {
|
|
documentName: requestDocumentName,
|
|
message: requestMessage,
|
|
requestedBy: "Utilisateur actuel",
|
|
requestDate: new Date(),
|
|
folder: importFolder || "Général",
|
|
role: selectedRole,
|
|
}
|
|
showNotification("success", `Demande de document "${requestDocumentName}" envoyée au rôle ${selectedRole}`)
|
|
const targetFolder = folders.find((f) => f.name === (importFolder || "Général"))
|
|
if (targetFolder) {
|
|
sendFolderChatNotification(
|
|
targetFolder.id,
|
|
`📋 Demande de document : "${requestDocumentName}"
|
|
Rôle ciblé : ${selectedRole}
|
|
Message : ${requestMessage || "Aucun message spécifique"}`,
|
|
"document_request",
|
|
)
|
|
}
|
|
console.log("Demande de document:", requestData)
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmImport = () => {
|
|
if (!importFiles || importFiles.length === 0) {
|
|
showNotification("error", "Veuillez sélectionner au moins un fichier à importer")
|
|
return
|
|
}
|
|
|
|
if (!importFolder.trim()) {
|
|
showNotification("error", "Veuillez sélectionner un dossier de destination")
|
|
return
|
|
}
|
|
|
|
const fileList = Array.from(importFiles)
|
|
const totalSize = fileList.reduce((sum, file) => sum + file.size, 0)
|
|
const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2)
|
|
|
|
showNotification("info", `Import en cours de ${fileList.length} fichier(s) (${totalSizeMB} MB)...`)
|
|
|
|
// Simuler l'import avec un délai
|
|
setTimeout(() => {
|
|
const newDocuments: Document[] = fileList.map((file, index) => ({
|
|
id: Math.max(...documents.map((d) => d.id)) + index + 1,
|
|
name: file.name,
|
|
type: file.name.split(".").pop()?.toUpperCase() || "FILE",
|
|
size: `${(file.size / (1024 * 1024)).toFixed(2)} MB`,
|
|
modified: new Date(),
|
|
created: new Date(),
|
|
author: "Utilisateur actuel",
|
|
folder: importFolder,
|
|
folderId: folders.find((f) => f.name === importFolder)?.id || "general",
|
|
tags: [],
|
|
status: "draft",
|
|
thumbnail: `/placeholder.svg?height=120&width=120&text=${file.name.split(".").pop()?.toUpperCase()}`,
|
|
description: importDescription || `Document importé : ${file.name}`,
|
|
version: "v1.0",
|
|
isValidated: false,
|
|
hasCertificate: false,
|
|
storageType: "temporary" as const,
|
|
permissions: {
|
|
canView: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canInvite: true,
|
|
canValidate: true,
|
|
canArchive: true,
|
|
canAnalyze: true,
|
|
},
|
|
}))
|
|
|
|
setDocuments((prev) => [...prev, ...newDocuments])
|
|
|
|
// Notification de succès
|
|
showNotification("success", `${fileList.length} document(s) importé(s) avec succès dans ${importFolder}`)
|
|
|
|
// Notification dans le chat du dossier
|
|
const targetFolder = folders.find((f) => f.name === importFolder)
|
|
if (targetFolder) {
|
|
const fileNames = fileList.map((f) => f.name).join(", ")
|
|
sendFolderChatNotification(
|
|
targetFolder.id,
|
|
`📁 ${fileList.length} nouveau(x) document(s) importé(s) :\n${fileNames}\n\nDescription : ${importDescription || "Aucune description"}`,
|
|
"documents_import",
|
|
)
|
|
}
|
|
|
|
// Mettre à jour les stats
|
|
setStats((prev) => ({
|
|
...prev,
|
|
total: prev.total + fileList.length,
|
|
temporary: prev.temporary + fileList.length,
|
|
}))
|
|
}, 2000)
|
|
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmValidate = () => {
|
|
if (!actionModal.documents || actionModal.documents.length === 0) return;
|
|
setDocuments((prev) => prev.map((doc) =>
|
|
actionModal.documents.some((d) => d.id === doc.id)
|
|
? { ...doc, isValidated: true, status: "validated", storageType: "permanent" }
|
|
: doc
|
|
));
|
|
showNotification("success", `${actionModal.documents.length} document(s) validé(s) et déplacé(s) en permanent`);
|
|
setActionModal({ type: null, document: null, documents: [] });
|
|
};
|
|
|
|
const confirmArchive = () => {
|
|
if (actionModal.document) {
|
|
const updatedDoc = {
|
|
...actionModal.document,
|
|
storageType: "permanent" as const,
|
|
status: "archived",
|
|
modified: new Date(),
|
|
}
|
|
setDocuments((prev) => prev.map((doc) => (doc.id === updatedDoc.id ? updatedDoc : doc)))
|
|
showNotification("success", `${actionModal.document.name} archivé vers le stockage permanent`)
|
|
|
|
let message = `📦 ${actionModal.document.name} a été archivé vers le stockage permanent (conservation: ${retentionPeriod} ans)`
|
|
if (archiveReason.trim()) {
|
|
message += ` - Raison: ${archiveReason}`
|
|
}
|
|
|
|
sendFolderChatNotification(actionModal.document.folderId, message, "archive", actionModal.document.name)
|
|
} else if (actionModal.documents.length > 0) {
|
|
const folderGroups = actionModal.documents.reduce(
|
|
(acc, doc) => {
|
|
if (!acc[doc.folderId]) acc[doc.folderId] = []
|
|
acc[doc.folderId].push(doc.name)
|
|
return acc
|
|
},
|
|
{} as { [folderId: string]: string[] },
|
|
)
|
|
|
|
Object.entries(folderGroups).forEach(([folderId, docNames]) => {
|
|
let message = `📦 ${docNames.length} document(s) archivé(s) vers le stockage permanent (conservation: ${retentionPeriod} ans) : ${docNames.join(", ")}`
|
|
if (archiveReason.trim()) {
|
|
message += ` - Raison: ${archiveReason}`
|
|
}
|
|
|
|
sendFolderChatNotification(folderId, message, "bulk_archive")
|
|
})
|
|
|
|
const docIds = actionModal.documents.map((doc) => doc.id)
|
|
setDocuments((prev) =>
|
|
prev.map((doc) =>
|
|
docIds.includes(doc.id)
|
|
? { ...doc, storageType: "permanent" as const, status: "archived", modified: new Date() }
|
|
: doc,
|
|
),
|
|
)
|
|
showNotification("success", `${actionModal.documents.length} document(s) archivé(s) vers le stockage permanent`)
|
|
setSelectedDocuments([])
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const confirmStorageConfig = () => {
|
|
if (actionModal.document) {
|
|
const updatedDoc = {
|
|
...actionModal.document,
|
|
temporaryStorageConfig: {
|
|
duration: Number.parseInt(storageDuration),
|
|
dataUsage: dataUsage,
|
|
thirdPartyAccess: thirdPartyAccess,
|
|
},
|
|
modified: new Date(),
|
|
}
|
|
setDocuments((prev) => prev.map((doc) => (doc.id === updatedDoc.id ? updatedDoc : doc)))
|
|
showNotification("success", `Configuration de stockage mise à jour pour ${actionModal.document.name}`)
|
|
|
|
const message =
|
|
`⚙️ Configuration de stockage temporaire mise à jour pour ${actionModal.document.name}:\n` +
|
|
`• Durée: ${storageDuration} jours\n` +
|
|
`• Usage: ${dataUsage}\n` +
|
|
`• Accès tiers: ${thirdPartyAccess}`
|
|
|
|
sendFolderChatNotification(actionModal.document.folderId, message, "storage_config", actionModal.document.name)
|
|
}
|
|
setActionModal({ type: null, document: null, documents: [] })
|
|
}
|
|
|
|
const filteredDocuments = documents
|
|
.filter((doc) => {
|
|
if (searchTerm && !doc.name.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
return false
|
|
}
|
|
if (filterType !== "all" && doc.type.toLowerCase() !== filterType.toLowerCase()) {
|
|
return false
|
|
}
|
|
if (filterAuthor !== "all" && doc.author !== filterAuthor) {
|
|
return false
|
|
}
|
|
if (filterStorage !== "all" && doc.storageType !== filterStorage) {
|
|
return false
|
|
}
|
|
if (filterFolder !== "all" && doc.folder !== filterFolder) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
.sort((a, b) => {
|
|
let aValue, bValue
|
|
switch (sortBy) {
|
|
case "name":
|
|
aValue = a.name.toLowerCase()
|
|
bValue = b.name.toLowerCase()
|
|
break
|
|
case "size":
|
|
aValue = Number.parseFloat(a.size.replace(/[^\d.]/g, ""))
|
|
bValue = Number.parseFloat(b.size.replace(/[^\d.]/g, ""))
|
|
break
|
|
case "author":
|
|
aValue = a.author.toLowerCase()
|
|
bValue = b.author.toLowerCase()
|
|
break
|
|
case "modified":
|
|
default:
|
|
aValue = a.modified.getTime()
|
|
bValue = b.modified.getTime()
|
|
break
|
|
}
|
|
|
|
if (sortOrder === "asc") {
|
|
return aValue > bValue ? 1 : -1
|
|
} else {
|
|
return aValue < bValue ? 1 : -1
|
|
}
|
|
})
|
|
|
|
const getFileIcon = (type: string) => {
|
|
switch (type.toLowerCase()) {
|
|
case "pdf":
|
|
return <FileText className="h-5 w-5 text-red-600" />
|
|
case "docx":
|
|
case "doc":
|
|
return <FileText className="h-5 w-5 text-blue-600" />
|
|
case "xlsx":
|
|
case "xls":
|
|
return <FileSpreadsheet className="h-5 w-5 text-green-600" />
|
|
case "pptx":
|
|
case "ppt":
|
|
return <FileText className="h-5 w-5 text-orange-600" />
|
|
case "png":
|
|
case "jpg":
|
|
case "jpeg":
|
|
return <FileImage className="h-5 w-5 text-purple-600" />
|
|
case "mp4":
|
|
case "avi":
|
|
return <FileVideo className="h-5 w-5 text-pink-600" />
|
|
case "zip":
|
|
case "rar":
|
|
return <Archive className="h-5 w-5 text-gray-600" />
|
|
default:
|
|
return <FileText className="h-5 w-5 text-gray-600" />
|
|
}
|
|
}
|
|
|
|
const isNewDocument = (doc: DocumentData) => {
|
|
const now = Date.now()
|
|
const diffMs = now - doc.modified.getTime()
|
|
const twoDaysMs = 2 * 24 * 60 * 60 * 1000
|
|
return diffMs <= twoDaysMs
|
|
}
|
|
|
|
const getStorageIcon = (storageType: string) => {
|
|
return storageType === "permanent" ? (
|
|
<Cloud className="h-4 w-4 text-blue-600" title="Stockage permanent" />
|
|
) : (
|
|
<HardDrive className="h-4 w-4 text-gray-600" title="Stockage temporaire" />
|
|
)
|
|
}
|
|
|
|
const getRoleIcon = (role: string) => {
|
|
switch (role) {
|
|
case "owner":
|
|
return <Crown className="h-4 w-4 text-yellow-600" />
|
|
case "editor":
|
|
return <Edit className="h-4 w-4 text-blue-600" />
|
|
case "validator":
|
|
return <ShieldCheck className="h-4 w-4 text-green-600" />
|
|
case "contributor":
|
|
return <UserPlus className="h-4 w-4 text-purple-600" />
|
|
case "viewer":
|
|
return <Eye className="h-4 w-4 text-gray-600" />
|
|
case "admin":
|
|
return <Shield className="h-4 w-4 text-red-600" />
|
|
case "manager":
|
|
return <Users className="h-4 w-4 text-orange-600" />
|
|
case "user":
|
|
return <User className="h-4 w-4 text-blue-600" />
|
|
case "guest":
|
|
return <User className="h-4 w-4 text-gray-400" />
|
|
default:
|
|
return <User className="h-4 w-4 text-gray-600" />
|
|
}
|
|
}
|
|
|
|
const getStatusBadge = (status: string, isValidated: boolean) => {
|
|
if (isValidated) {
|
|
return <Badge className="bg-green-100 text-green-800 border-green-200">Validé</Badge>
|
|
}
|
|
|
|
switch (status) {
|
|
case "pending":
|
|
return <Badge className="bg-orange-100 text-orange-800 border-orange-200">En attente</Badge>
|
|
case "draft":
|
|
return <Badge className="bg-gray-100 text-gray-800 border-gray-200">Brouillon</Badge>
|
|
case "rejected":
|
|
return <Badge className="bg-red-100 text-red-800 border-red-200">Rejeté</Badge>
|
|
case "archived":
|
|
return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Archivé</Badge>
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const formatDate = (date: Date) => {
|
|
const now = new Date()
|
|
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60)
|
|
|
|
if (diffInHours < 1) {
|
|
return "Il y a quelques minutes"
|
|
} else if (diffInHours < 24) {
|
|
return `Il y a ${Math.floor(diffInHours)} heure${Math.floor(diffInHours) > 1 ? "s" : ""}`
|
|
} else if (diffInHours < 48) {
|
|
return "Hier"
|
|
} else {
|
|
return date.toLocaleDateString("fr-FR")
|
|
}
|
|
}
|
|
|
|
const toggleDocumentSelection = (docId: number) => {
|
|
setSelectedDocuments((prev) => (prev.includes(docId) ? prev.filter((id) => id !== docId) : [...prev, docId]))
|
|
}
|
|
|
|
const selectAllDocuments = () => {
|
|
if (selectedDocuments.length === filteredDocuments.length) {
|
|
setSelectedDocuments([])
|
|
} else {
|
|
setSelectedDocuments(filteredDocuments.map((doc) => doc.id))
|
|
}
|
|
}
|
|
|
|
const handleMoveDocuments = (docIds: number[]) => {
|
|
if (!docIds || docIds.length === 0) return;
|
|
const docs = docIds.map(id => documents.find(d => d.id === id)).filter(Boolean);
|
|
setNewFolderName("");
|
|
setActionModal({ type: "move", document: docs[0], documents: docs });
|
|
};
|
|
|
|
const handleChangeStorage = (docIds: number[]) => {
|
|
if (!docIds || docIds.length === 0) return;
|
|
const docs = docIds.map(id => documents.find(d => d.id === id)).filter(Boolean);
|
|
setStorageDuration("30");
|
|
setDataUsage("");
|
|
setThirdPartyAccess("");
|
|
setActionModal({ type: "storage_config", document: docs[0], documents: docs });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Notification */}
|
|
{notification && (
|
|
<div
|
|
className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg flex items-center space-x-2 ${
|
|
notification.type === "success"
|
|
? "bg-green-100 text-green-800 border border-green-200"
|
|
: notification.type === "error"
|
|
? "bg-red-100 text-red-800 border border-red-200"
|
|
: "bg-blue-100 text-blue-800 border border-blue-200"
|
|
}`}
|
|
>
|
|
{notification.type === "success" && <CheckCircle className="h-5 w-5" />}
|
|
{notification.type === "error" && <XCircle className="h-5 w-5" />}
|
|
{notification.type === "info" && <Info className="h-5 w-5" />}
|
|
<span>{notification.message}</span>
|
|
<Button variant="ghost" size="sm" onClick={() => setNotification(null)}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
Documents
|
|
{filterFolder !== "all" && (
|
|
<span className="text-lg font-normal text-gray-600 ml-2">
|
|
- {filterFolder}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="ml-2 h-6 w-6 p-0"
|
|
onClick={() => {
|
|
setFilterFolder("all")
|
|
window.history.replaceState({}, "", "/dashboard/documents")
|
|
}}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</span>
|
|
)}
|
|
</h1>
|
|
<p className="text-gray-600 mt-1">
|
|
{filterFolder !== "all" ? `Documents du dossier ${filterFolder}` : "Gérez vos documents et fichiers"}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3 mt-4 sm:mt-0">
|
|
<Button variant="outline" size="sm" onClick={handleRequestDocument}>
|
|
<FileQuestion className="h-4 w-4 mr-2" />
|
|
Demander un document
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={handleImportDocuments}>
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
Importer
|
|
</Button>
|
|
<Button size="sm">
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Nouveau document
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
|
|
<div className="flex flex-col sm:flex-row sm:items-center space-y-3 sm:space-y-0 sm:space-x-4">
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className={showFilters ? "bg-blue-50 text-blue-700 border-blue-200" : ""}
|
|
>
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
Filtres
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex items-center space-x-2">
|
|
<Label htmlFor="sort" className="text-sm">
|
|
Trier par:
|
|
</Label>
|
|
<Select value={sortBy} onValueChange={setSortBy}>
|
|
<SelectTrigger className="w-32">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="modified">Modifié</SelectItem>
|
|
<SelectItem value="name">Nom</SelectItem>
|
|
<SelectItem value="size">Taille</SelectItem>
|
|
<SelectItem value="author">Auteur</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button variant="ghost" size="sm" onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}>
|
|
{sortOrder === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Vue grille supprimée: forcer la vue liste uniquement */}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Advanced Filters */}
|
|
{showFilters && (
|
|
<div className="mt-4 pt-4 border-t">
|
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
<div>
|
|
<Label htmlFor="filterType" className="text-sm font-medium">
|
|
Type de fichier
|
|
</Label>
|
|
<Select value={filterType} onValueChange={setFilterType}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les types</SelectItem>
|
|
<SelectItem value="pdf">PDF</SelectItem>
|
|
<SelectItem value="docx">Word</SelectItem>
|
|
<SelectItem value="xlsx">Excel</SelectItem>
|
|
<SelectItem value="pptx">PowerPoint</SelectItem>
|
|
<SelectItem value="png">Images</SelectItem>
|
|
<SelectItem value="mp4">Vidéos</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="filterAuthor" className="text-sm font-medium">
|
|
Auteur
|
|
</Label>
|
|
<Select value={filterAuthor} onValueChange={setFilterAuthor}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les auteurs</SelectItem>
|
|
<SelectItem value="Marie Dubois">Marie Dubois</SelectItem>
|
|
<SelectItem value="Sophie Laurent">Sophie Laurent</SelectItem>
|
|
<SelectItem value="Jean Martin">Jean Martin</SelectItem>
|
|
<SelectItem value="Pierre Durand">Pierre Durand</SelectItem>
|
|
<SelectItem value="Admin Système">Admin Système</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="filterStorage" className="text-sm font-medium">
|
|
Type de stockage
|
|
</Label>
|
|
<Select value={filterStorage} onValueChange={setFilterStorage}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les stockages</SelectItem>
|
|
<SelectItem value="temporary">
|
|
<div className="flex items-center space-x-2">
|
|
<HardDrive className="h-4 w-4" />
|
|
<span>Temporaire</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="permanent">
|
|
<div className="flex items-center space-x-2">
|
|
<Cloud className="h-4 w-4" />
|
|
<span>Permanent</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="filterFolder" className="text-sm font-medium">
|
|
Dossier
|
|
</Label>
|
|
<Select value={filterFolder} onValueChange={setFilterFolder}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les dossiers</SelectItem>
|
|
{folders.map((folder) => (
|
|
<SelectItem key={folder.id} value={folder.name}>
|
|
<div className="flex items-center space-x-2">
|
|
<Folder className="h-4 w-4" />
|
|
<span>{folder.name}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-end">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setFilterType("all")
|
|
setFilterAuthor("all")
|
|
setFilterStorage("all")
|
|
setFilterFolder("all")
|
|
setSearchTerm("")
|
|
window.history.replaceState({}, "", "/dashboard/documents")
|
|
}}
|
|
>
|
|
Réinitialiser
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bulk Actions minimalistes: certificats et rôles uniquement */}
|
|
{selectedDocuments.length > 0 && (
|
|
<Card className="bg-blue-50 border-blue-200">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<Checkbox
|
|
checked={selectedDocuments.length === filteredDocuments.length}
|
|
onCheckedChange={selectAllDocuments}
|
|
/>
|
|
<span className="text-sm font-medium">
|
|
{selectedDocuments.length} document{selectedDocuments.length > 1 ? "s" : ""} sélectionné
|
|
{selectedDocuments.length > 1 ? "s" : ""}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const docsToDownload = documents.filter((d) => selectedDocuments.includes(d.id))
|
|
if (docsToDownload.length === 0) {
|
|
showNotification("info", "Aucun document à télécharger pour la sélection")
|
|
return
|
|
}
|
|
docsToDownload.forEach((d) => handleDownloadDocument(d))
|
|
}}
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
handleRequestDocument(selectedDocuments)
|
|
}}
|
|
>
|
|
<FileQuestion className="h-4 w-4 mr-2" />
|
|
Demander
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
handleValidateDocuments(selectedDocuments)
|
|
}}
|
|
>
|
|
<CheckCircle className="h-4 w-4 mr-2" />
|
|
Valider
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
handleMoveDocuments(selectedDocuments)
|
|
}}
|
|
>
|
|
<FolderOpen className="h-4 w-4 mr-2" />
|
|
Déplacer
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
handleChangeStorage(selectedDocuments)
|
|
}}
|
|
>
|
|
<HardDrive className="h-4 w-4 mr-2" />
|
|
Conservation
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const docsToDownload = documents.filter((d) => selectedDocuments.includes(d.id) && d.hasCertificate)
|
|
if (docsToDownload.length === 0) {
|
|
showNotification("info", "Aucun certificat à télécharger pour la sélection")
|
|
return
|
|
}
|
|
docsToDownload.forEach((d) => handleDownloadCertificate(d))
|
|
}}
|
|
>
|
|
<ShieldCheck className="h-4 w-4 mr-2" />
|
|
Certificats
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const first = documents.find((d) => selectedDocuments.includes(d.id))
|
|
if (first) {
|
|
handleManageDocumentRoles(first)
|
|
} else {
|
|
showNotification("info", "Sélectionnez au moins un document")
|
|
}
|
|
}}
|
|
>
|
|
<Users className="h-4 w-4 mr-2" />
|
|
Rôles
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Documents List/Grid */}
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
{viewMode === "list" ? (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="text-left py-3 px-4 w-8">
|
|
<Checkbox
|
|
checked={selectedDocuments.length === filteredDocuments.length}
|
|
onCheckedChange={selectAllDocuments}
|
|
/>
|
|
</th>
|
|
<th className="text-left py-3 px-4 font-medium text-gray-900">Nom</th>
|
|
<th className="text-left py-3 px-4 font-medium text-gray-900">Taille</th>
|
|
<th className="text-left py-3 px-4 font-medium text-gray-900">Modifié</th>
|
|
<th className="text-left py-3 px-4 font-medium text-gray-900">Auteur</th>
|
|
<th className="text-left py-3 px-4 font-medium text-gray-900">Dossier</th>
|
|
|
|
<th className="text-left py-3 px-4 font-medium text-gray-900">Statut</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredDocuments.map((doc) => (
|
|
<tr key={doc.id} className="border-b hover:bg-gray-50">
|
|
<td className="py-3 px-4">
|
|
<Checkbox
|
|
checked={selectedDocuments.includes(doc.id)}
|
|
onCheckedChange={() => toggleDocumentSelection(doc.id)}
|
|
/>
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<div className="flex items-center space-x-3">
|
|
{getFileIcon(doc.type)}
|
|
<div className="flex items-center space-x-2">
|
|
<span className="font-medium text-gray-900">{doc.name}</span>
|
|
{isNewDocument(doc) && (
|
|
<Badge className="bg-blue-100 text-blue-700 border-blue-200">NEW</Badge>
|
|
)}
|
|
{getStorageIcon(doc.storageType)}
|
|
{doc.isValidated && <ShieldCheck className="h-4 w-4 text-green-600" />}
|
|
{doc.temporaryStorageConfig && (
|
|
<AlertTriangle
|
|
className="h-4 w-4 text-orange-500"
|
|
title="Configuration de stockage temporaire définie"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="py-3 px-4 text-gray-600">{doc.size}</td>
|
|
<td className="py-3 px-4 text-gray-600">{formatDate(doc.modified)}</td>
|
|
<td className="py-3 px-4 text-gray-600">{doc.author}</td>
|
|
<td className="py-3 px-4 text-gray-600">{doc.folder}</td>
|
|
|
|
<td className="py-3 px-4">{getStatusBadge(doc.status, doc.isValidated)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<div className="p-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
|
{filteredDocuments.map((doc) => (
|
|
<div
|
|
key={doc.id}
|
|
className={`relative group border rounded-lg p-4 hover:shadow-md transition-shadow ${
|
|
selectedDocuments.includes(doc.id) ? "bg-blue-50 border-blue-200" : "bg-white"
|
|
}`}
|
|
>
|
|
<div className="absolute top-2 left-2">
|
|
<Checkbox
|
|
checked={selectedDocuments.includes(doc.id)}
|
|
onCheckedChange={() => toggleDocumentSelection(doc.id)}
|
|
/>
|
|
</div>
|
|
<div className="absolute top-2 right-2 flex items-center space-x-1">
|
|
{isNewDocument(doc) && (
|
|
<Badge className="bg-blue-100 text-blue-700 border-blue-200">NEW</Badge>
|
|
)}
|
|
{getStorageIcon(doc.storageType)}
|
|
{doc.isValidated && <ShieldCheck className="h-4 w-4 text-green-600" />}
|
|
{doc.temporaryStorageConfig && (
|
|
<AlertTriangle
|
|
className="h-4 w-4 text-orange-500"
|
|
title="Configuration de stockage temporaire définie"
|
|
/>
|
|
)}
|
|
{doc.storageType === "temporary" && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleConfigureStorage(doc)}
|
|
className="h-8 w-8 p-0"
|
|
title="Configurer le stockage temporaire"
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleManageDocumentRoles(doc)}
|
|
className="h-8 w-8 p-0"
|
|
title="Gérer les rôles du document"
|
|
>
|
|
<Users className="h-4 w-4" />
|
|
</Button>
|
|
{doc.hasCertificate && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDownloadCertificate(doc)}
|
|
className="h-8 w-8 p-0"
|
|
title="Télécharger le certificat blockchain"
|
|
>
|
|
<ShieldCheck className="h-4 w-4 text-green-600" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center space-y-3 mt-6">
|
|
<div className="w-16 h-16 flex items-center justify-center bg-gray-100 rounded-lg">
|
|
{getFileIcon(doc.type)}
|
|
</div>
|
|
|
|
<div className="text-center space-y-1">
|
|
<h3 className="font-medium text-gray-900 text-sm truncate w-full" title={doc.name}>
|
|
{doc.name}
|
|
</h3>
|
|
<p className="text-xs text-gray-500">{doc.size}</p>
|
|
<p className="text-xs text-gray-500">{formatDate(doc.modified)}</p>
|
|
<p className="text-xs text-gray-500 flex items-center justify-center space-x-1">
|
|
{getStorageIcon(doc.storageType)}
|
|
<span>{doc.storageType === "permanent" ? "Permanent" : "Temporaire"}</span>
|
|
{doc.storageType === "temporary" && doc.temporaryStorageConfig && (
|
|
<span className="text-orange-600">({doc.temporaryStorageConfig.duration}j)</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{getStatusBadge(doc.status, doc.isValidated) && (
|
|
<div className="flex justify-center">{getStatusBadge(doc.status, doc.isValidated)}</div>
|
|
)}
|
|
|
|
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Button variant="ghost" size="sm" onClick={() => handleViewDocument(doc)}>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDownloadDocument(doc)}>
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
{doc.permissions.canAnalyze && (
|
|
<Button variant="ghost" size="sm" onClick={() => handleAIAnalysis(doc)}>
|
|
<Brain className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{filteredDocuments.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<FileText className="h-12 w-12 mx-auto text-gray-400 mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Aucun document trouvé</h3>
|
|
<p className="text-gray-600 mb-4">
|
|
{searchTerm ||
|
|
filterType !== "all" ||
|
|
filterAuthor !== "all" ||
|
|
filterStorage !== "all" ||
|
|
filterFolder !== "all"
|
|
? "Essayez de modifier vos critères de recherche"
|
|
: "Commencez par importer votre premier document"}
|
|
</p>
|
|
<Button onClick={handleImportDocuments}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Importer des documents
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Modals */}
|
|
{actionModal.type && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
|
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
{/* Import Modal */}
|
|
{actionModal.type === "import" && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Importer des documents</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<div className="flex items-center space-x-2">
|
|
<Upload className="h-4 w-4 text-blue-600" />
|
|
<span className="text-sm font-medium text-blue-900">Import de documents</span>
|
|
</div>
|
|
<p className="text-sm text-blue-800 mt-1">
|
|
Sélectionnez les fichiers à importer et choisissez le dossier de destination.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="importFiles" className="text-sm font-medium text-gray-700">
|
|
Fichiers à importer *
|
|
</Label>
|
|
<Input
|
|
id="importFiles"
|
|
type="file"
|
|
multiple
|
|
onChange={(e) => setImportFiles(e.target.files)}
|
|
className="mt-1"
|
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.png,.jpg,.jpeg,.gif,.mp4,.avi,.zip,.rar"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Formats supportés: PDF, Word, Excel, PowerPoint, Images, Vidéos, Archives
|
|
</p>
|
|
{importFiles && importFiles.length > 0 && (
|
|
<div className="mt-2 p-2 bg-gray-50 rounded text-sm">
|
|
<p className="font-medium text-gray-700">{importFiles.length} fichier(s) sélectionné(s):</p>
|
|
<ul className="mt-1 space-y-1 max-h-32 overflow-y-auto">
|
|
{Array.from(importFiles).map((file, index) => (
|
|
<li key={index} className="text-gray-600 flex justify-between">
|
|
<span className="truncate">{file.name}</span>
|
|
<span className="text-xs text-gray-500 ml-2">
|
|
{(file.size / (1024 * 1024)).toFixed(2)} MB
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="importFolder" className="text-sm font-medium text-gray-700">
|
|
Dossier de destination *
|
|
</Label>
|
|
<Select value={importFolder} onValueChange={setImportFolder}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Choisir un dossier" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{folders.map((folder) => (
|
|
<SelectItem key={folder.id} value={folder.name}>
|
|
<div className="flex items-center space-x-2">
|
|
<Folder className="h-4 w-4" />
|
|
<span>{folder.name}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="importDescription" className="text-sm font-medium text-gray-700">
|
|
Description (optionnel)
|
|
</Label>
|
|
<Textarea
|
|
id="importDescription"
|
|
value={importDescription}
|
|
onChange={(e) => setImportDescription(e.target.value)}
|
|
placeholder="Ajouter une description pour ces documents..."
|
|
rows={3}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<AlertTriangle className="h-4 w-4 text-yellow-600 mt-0.5" />
|
|
<div className="text-sm text-yellow-800">
|
|
<p className="font-medium">Information importante</p>
|
|
<p>
|
|
Les documents importés seront stockés en mode temporaire par défaut. Vous pourrez les
|
|
configurer et les valider après l'import.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={confirmImport} disabled={!importFiles || !importFolder}>
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
Importer{" "}
|
|
{importFiles ? `(${importFiles.length} fichier${importFiles.length > 1 ? "s" : ""})` : ""}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Request Document Modal */}
|
|
{actionModal.type === "request" && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Demander un document</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<div className="flex items-center space-x-2">
|
|
<FileQuestion className="h-4 w-4 text-blue-600" />
|
|
<span className="text-sm font-medium text-blue-900">Demande de document</span>
|
|
</div>
|
|
<p className="text-sm text-blue-800 mt-1">
|
|
Spécifiez le document dont vous avez besoin et ajoutez un message explicatif.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="requestDocumentName" className="text-sm font-medium text-gray-700">
|
|
Nom du document demandé *
|
|
</Label>
|
|
<Input
|
|
id="requestDocumentName"
|
|
value={requestDocumentName}
|
|
onChange={(e) => setRequestDocumentName(e.target.value)}
|
|
placeholder="Ex: Contrat de prestation, Facture janvier 2024..."
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="requestFolder" className="text-sm font-medium text-gray-700">
|
|
Dossier de destination
|
|
</Label>
|
|
<Select value={importFolder} onValueChange={setImportFolder}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Choisir un dossier (optionnel)" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{folders.map((folder) => (
|
|
<SelectItem key={folder.id} value={folder.name}>
|
|
<div className="flex items-center space-x-2">
|
|
<Folder className="h-4 w-4" />
|
|
<span>{folder.name}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="requestRole" className="text-sm font-medium text-gray-700">
|
|
Rôle de destination *
|
|
</Label>
|
|
<Select id="requestRole" value={selectedRole} onValueChange={setSelectedRole}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Choisir un rôle" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="owners">owners</SelectItem>
|
|
<SelectItem value="final_clients">final_clients</SelectItem>
|
|
<SelectItem value="process_updators">process_updators</SelectItem>
|
|
<SelectItem value="backups">backups</SelectItem>
|
|
<SelectItem value="digital_legal_requests">digital_legal_requests</SelectItem>
|
|
<SelectItem value="support_n1">support_n1</SelectItem>
|
|
<SelectItem value="support_n2">support_n2</SelectItem>
|
|
<SelectItem value="support_n3">support_n3</SelectItem>
|
|
<SelectItem value="security_alert">security_alert</SelectItem>
|
|
<SelectItem value="security_organization_board">security_organization_board</SelectItem>
|
|
<SelectItem value="opposit_right">opposit_right</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="requestMessage" className="text-sm font-medium text-gray-700">
|
|
Message explicatif
|
|
</Label>
|
|
<Textarea
|
|
id="requestMessage"
|
|
value={requestMessage}
|
|
onChange={(e) => setRequestMessage(e.target.value)}
|
|
placeholder="Expliquez pourquoi vous avez besoin de ce document, son usage prévu, l'urgence..."
|
|
rows={4}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<Info className="h-4 w-4 text-green-600 mt-0.5" />
|
|
<div className="text-sm text-green-800">
|
|
<p className="font-medium">Processus de demande</p>
|
|
<p>
|
|
Votre demande sera envoyée aux responsables du dossier concerné. Vous recevrez une
|
|
notification dès qu'une réponse sera disponible.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={confirmRequest} disabled={!requestDocumentName.trim() || !selectedRole}>
|
|
<FileQuestion className="h-4 w-4 mr-2" />
|
|
Envoyer la demande
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* View Document Modal */}
|
|
{actionModal.type === "view" && actionModal.document && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Aperçu du document</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-16 h-16 flex items-center justify-center bg-gray-100 rounded-lg">
|
|
{getFileIcon(actionModal.document.type)}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-gray-900">{actionModal.document.name}</h4>
|
|
<p className="text-sm text-gray-600">
|
|
{actionModal.document.type} • {actionModal.document.size}
|
|
</p>
|
|
<div className="flex items-center space-x-2 mt-1">
|
|
{getStorageIcon(actionModal.document.storageType)}
|
|
<span className="text-sm text-gray-600">
|
|
{actionModal.document.storageType === "permanent"
|
|
? "Stockage permanent"
|
|
: "Stockage temporaire"}
|
|
</span>
|
|
{actionModal.document.temporaryStorageConfig && (
|
|
<span className="text-sm text-orange-600">
|
|
({actionModal.document.temporaryStorageConfig.duration} jours)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Auteur</Label>
|
|
<p className="text-sm text-gray-900">{actionModal.document.author}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Dossier</Label>
|
|
<p className="text-sm text-gray-900">{actionModal.document.folder}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Créé le</Label>
|
|
<p className="text-sm text-gray-900">
|
|
{actionModal.document.created.toLocaleDateString("fr-FR")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Modifié le</Label>
|
|
<p className="text-sm text-gray-900">
|
|
{actionModal.document.modified.toLocaleDateString("fr-FR")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Version</Label>
|
|
<p className="text-sm text-gray-900">{actionModal.document.version}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Statut</Label>
|
|
<div className="mt-1">
|
|
{getStatusBadge(actionModal.document.status, actionModal.document.isValidated)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{actionModal.document.description && (
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Description</Label>
|
|
<p className="text-sm text-gray-900 mt-1">{actionModal.document.description}</p>
|
|
</div>
|
|
)}
|
|
|
|
{actionModal.document.summary && (
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Résumé</Label>
|
|
<p className="text-sm text-gray-900 mt-1">{actionModal.document.summary}</p>
|
|
</div>
|
|
)}
|
|
|
|
{actionModal.document.temporaryStorageConfig && (
|
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
|
<h5 className="font-medium text-orange-900 mb-2">Configuration du stockage temporaire</h5>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<Label className="text-sm font-medium text-orange-700">Durée de conservation</Label>
|
|
<p className="text-sm text-orange-900">
|
|
{actionModal.document.temporaryStorageConfig.duration} jours
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-orange-700">Usage de la donnée</Label>
|
|
<p className="text-sm text-orange-900">
|
|
{actionModal.document.temporaryStorageConfig.dataUsage}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-medium text-orange-700">Accès tiers</Label>
|
|
<p className="text-sm text-orange-900">
|
|
{actionModal.document.temporaryStorageConfig.thirdPartyAccess}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{actionModal.document.tags.length > 0 && (
|
|
<div>
|
|
<Label className="text-sm font-medium text-gray-700">Tags</Label>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{actionModal.document.tags.map((tag) => (
|
|
<Badge key={tag} variant="outline" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
|
<Button variant="outline" onClick={() => handleDownloadDocument(actionModal.document!)}>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Récupérer
|
|
</Button>
|
|
{actionModal.document.permissions.canAnalyze && (
|
|
<Button variant="outline" onClick={() => handleAIAnalysis(actionModal.document!)}>
|
|
<Brain className="h-4 w-4 mr-2" />
|
|
Analyse IA
|
|
</Button>
|
|
)}
|
|
{actionModal.document.hasCertificate && (
|
|
<Button variant="outline" onClick={() => handleViewCertificate(actionModal.document!)}>
|
|
<ShieldCheck className="h-4 w-4 mr-2" />
|
|
Certificat
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Storage Configuration Modal */}
|
|
{actionModal.type === "storage_config" && actionModal.document && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Configuration du stockage temporaire</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<div className="flex items-center space-x-2">
|
|
<Calendar className="h-4 w-4 text-blue-600" />
|
|
<span className="text-sm font-medium text-blue-900">Document: {actionModal.document.name}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="storageDuration" className="text-sm font-medium text-gray-700">
|
|
Durée de conservation (en jours)
|
|
</Label>
|
|
<Input
|
|
id="storageDuration"
|
|
type="number"
|
|
value={storageDuration}
|
|
onChange={(e) => setStorageDuration(e.target.value)}
|
|
placeholder="30"
|
|
min="1"
|
|
max="3650"
|
|
className="mt-1"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Durée maximale recommandée: 365 jours pour les documents temporaires
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="dataUsage" className="text-sm font-medium text-gray-700">
|
|
Usage de la donnée
|
|
</Label>
|
|
<Textarea
|
|
id="dataUsage"
|
|
value={dataUsage}
|
|
onChange={(e) => setDataUsage(e.target.value)}
|
|
placeholder="Décrivez l'usage prévu de ce document..."
|
|
rows={3}
|
|
className="mt-1"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Précisez le contexte d'utilisation et la finalité du document
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="thirdPartyAccess" className="text-sm font-medium text-gray-700">
|
|
Tiers ayant potentiellement accès
|
|
</Label>
|
|
<Textarea
|
|
id="thirdPartyAccess"
|
|
value={thirdPartyAccess}
|
|
onChange={(e) => setThirdPartyAccess(e.target.value)}
|
|
placeholder="Listez les personnes ou organisations qui pourraient avoir accès..."
|
|
rows={3}
|
|
className="mt-1"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Incluez les collaborateurs, partenaires, prestataires externes, etc.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<AlertTriangle className="h-4 w-4 text-yellow-600 mt-0.5" />
|
|
<div className="text-sm text-yellow-800">
|
|
<p className="font-medium">Information importante</p>
|
|
<p>
|
|
Cette configuration aide à respecter les obligations RGPD et à gérer la durée de vie des
|
|
données temporaires.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={confirmStorageConfig}>
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
Enregistrer la configuration
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Certificate Modal */}
|
|
{actionModal.type === "certificate" && actionModal.document && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Certificat de validation</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setActionModal({ type: null, document: null, documents: [] })}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<div className="flex items-center space-x-3">
|
|
<ShieldCheck className="h-8 w-8 text-green-600" />
|
|
<div>
|
|
<h4 className="font-semibold text-green-900">Document certifié</h4>
|
|
<p className="text-sm text-green-700">Ce document a été validé et certifié numériquement</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h5 className="font-medium text-gray-900 mb-2">Informations du document</h5>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Nom :</span>
|
|
<span className="font-medium">{actionModal.document.name}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Version :</span>
|
|
<span className="font-medium">{actionModal.document.version}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Taille :</span>
|
|
<span className="font-medium">{actionModal.document.size}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Hash SHA-256 :</span>
|
|
<span className="font-mono text-xs bg-white p-1 rounded">
|
|
{Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h5 className="font-medium text-gray-900 mb-2">Certificat numérique</h5>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Émis le :</span>
|
|
<span className="font-medium">
|
|
{actionModal.document.modified.toLocaleDateString("fr-FR")}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Validé par :</span>
|
|
<span className="font-medium">{actionModal.document.author}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Autorité :</span>
|
|
<span className="font-medium">DocV Certification</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">ID Certificat :</span>
|
|
<span className="font-mono text-xs bg-white p-1 rounded">
|
|
CERT-{actionModal.document.id}-{new Date().getFullYear()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<h5 className="font-medium text-blue-900 mb-2">Détails de la validation</h5>
|
|
<div className="space-y-2 text-sm text-blue-800">
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
<span>Intégrité du document vérifiée</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
<span>Signature numérique valide</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
<span>Horodatage certifié</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
<span>Conformité RGPD validée</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-r from-green-50 to-blue-50 p-4 rounded-lg border border-green-200">
|
|
<h5 className="font-medium text-gray-900 mb-2">Chaîne de confiance blockchain</h5>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
|
<span className="text-gray-700">
|
|
Block #{Math.floor(Math.random() * 1000000)} - Hash: 0x
|
|
{Math.random().toString(16).substring(2, 10)}...
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
|
|
<span className="text-gray-700">
|
|
Transaction confirmée avec {Math.floor(Math.random() * 50) + 10} confirmations
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
|
|
<span className="text-gray-700">
|
|
Stockage distribué sur {Math.floor(Math.random() * 5) + 3} nœuds souverains
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
// Simuler le téléchargement du certificat
|
|
showNotification("success", `Certificat de ${actionModal.document!.name} téléchargé`)
|
|
sendFolderChatNotification(
|
|
actionModal.document!.folderId,
|
|
`📜 Certificat de validation téléchargé pour ${actionModal.document!.name}`,
|
|
"certificate_download",
|
|
actionModal.document!.name,
|
|
)
|
|
}}
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger le certificat (.pdf)
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
// Simuler la vérification en ligne
|
|
showNotification("info", "Vérification en ligne du certificat...")
|
|
setTimeout(() => {
|
|
showNotification("success", "Certificat vérifié avec succès")
|
|
}, 2000)
|
|
}}
|
|
>
|
|
<ShieldCheck className="h-4 w-4 mr-2" />
|
|
Vérifier en ligne
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Move Modal */}
|
|
{actionModal.type === "move" && actionModal.documents.length > 0 && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Déplacer le(s) document(s)</h3>
|
|
<Button variant="ghost" size="sm" onClick={() => setActionModal({ type: null, document: null, documents: [] })}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<Label htmlFor="moveFolder" className="text-sm font-medium text-gray-700">Dossier de destination</Label>
|
|
<Select id="moveFolder" value={newFolderName} onValueChange={setNewFolderName}>
|
|
<SelectTrigger className="w-full mt-1">
|
|
<SelectValue placeholder="Choisir un dossier" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{folders.map((f) => (
|
|
<SelectItem key={f.id} value={f.name}>{f.name}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<div className="flex justify-end space-x-2">
|
|
<Button variant="outline" onClick={() => setActionModal({ type: null, document: null, documents: [] })}>Annuler</Button>
|
|
<Button onClick={confirmMove} disabled={!newFolderName}>Déplacer</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Validate Modal */}
|
|
{actionModal.type === "validate" && actionModal.documents.length > 0 && (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Valider le(s) document(s)</h3>
|
|
<Button variant="ghost" size="sm" onClick={() => setActionModal({ type: null, document: null, documents: [] })}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<p>Souhaitez-vous valider ou refuser la sélection ?</p>
|
|
<div className="flex justify-end space-x-2">
|
|
<Button variant="outline" onClick={() => setActionModal({ type: null, document: null, documents: [] })}>Annuler</Button>
|
|
<Button onClick={confirmValidate}>Valider</Button>
|
|
<Button variant="destructive" onClick={() => {
|
|
if (!actionModal.documents || actionModal.documents.length === 0) return;
|
|
setDocuments((prev) => prev.map((doc) =>
|
|
actionModal.documents.some((d) => d.id === doc.id)
|
|
? { ...doc, isValidated: false, status: "rejected" }
|
|
: doc
|
|
));
|
|
showNotification("info", `${actionModal.documents.length} document(s) refusé(s)`);
|
|
setActionModal({ type: null, document: null, documents: [] });
|
|
}}>Refuser</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Other existing modals would continue here... */}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|