2025-10-23 16:41:57 +02:00

1465 lines
66 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect, useCallback } from "react"
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 {
Folder,
Search,
FolderPlus,
Clock,
ChevronRight,
SortAsc,
SortDesc,
X,
} from "lucide-react"
import { FolderData, FolderCreated, FolderPrivateFields, setDefaultFolderRoles } from "@/lib/4nk/models/FolderData"
import MessageBus from "@/lib/4nk/MessageBus"
import { iframeUrl } from "@/app/page"
import UserStore from "@/lib/4nk/UserStore"
import FolderModal from "@/components/4nk/FolderModal"
import AuthModal from "@/components/4nk/AuthModal"
import Iframe from "@/components/4nk/Iframe"
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
export default function FoldersPage() {
const [searchTerm, setSearchTerm] = useState("")
const [sortBy, setSortBy] = useState("updated_at")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
const [currentPath, setCurrentPath] = useState<string[]>(["Racine"])
const [folderType, setFolderType] = useState<FolderType | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
// 4NK Integration states
const [isConnected, setIsConnected] = useState(false)
const [showAuthModal, setShowAuthModal] = useState(false)
const [processes, setProcesses] = useState<any>(null)
const [myProcesses, setMyProcesses] = useState<string[]>([])
const [userPairingId, setUserPairingId] = useState<string | null>(null)
const [folderProcesses, setFolderProcesses] = useState<any>(null)
const [myFolderProcesses, setMyFolderProcesses] = useState<string[]>([])
const [folderPrivateData, setFolderPrivateData] = useState<Record<string, Record<string, any>>>({})
const [loadingFolders, setLoadingFolders] = useState(false)
// Modal states
const [notification, setNotification] = useState<{ type: "success" | "error" | "info"; message: string } | null>(null)
const [folders, setFolders] = useState<FolderData[]>([])
const [stats, setStats] = useState({
total: 0
})
// Notification system
const showNotification = (type: "success" | "error" | "info", message: string) => {
setNotification({ type, message })
setTimeout(() => setNotification(null), 5000)
}
// Function to fetch folder private data
const fetchFolderPrivateData = async (processId: string, stateId: string) => {
if (!myFolderProcesses.includes(processId)) return;
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const data = await messageBus.getData(processId, stateId);
setFolderPrivateData(prev => ({ ...prev, [stateId]: data }));
} catch (err) {
console.error('Error fetching folder private data:', err);
}
};
// Function to load folders from 4NK processes (adapted to new FolderData model)
const loadFoldersFrom4NK = useCallback(() => {
if (!folderProcesses) return;
const folderData: FolderData[] = [];
let hasAllPrivateData = true;
let hasFoldersToLoad = false;
Object.entries(folderProcesses).forEach(([processId, process]: [string, any]) => {
// Only include processes that belong to the user (myFolderProcesses)
if (!myFolderProcesses.includes(processId)) return;
// Check if this process has a folderNumber in pcd_commitment
const latestState = process.states[0];
if (!latestState) return;
const folderNumber = latestState.pcd_commitment?.folderNumber;
if (!folderNumber) return; // Skip processes without folderNumber
hasFoldersToLoad = true; // We have at least one folder to load
// Get private data for this state if available
const privateData = folderPrivateData[latestState.state_id];
// If we don't have private data yet, trigger fetch and mark as incomplete
if (!privateData) {
hasAllPrivateData = false;
setTimeout(() => fetchFolderPrivateData(processId, latestState.state_id), 0);
return; // Skip creating folder until we have private data
}
// Create folder with new simplified model
const folder: FolderData = {
folderNumber: folderNumber,
name: privateData.name || `Dossier ${folderNumber}`,
description: privateData.description || '',
created_at: privateData.created_at || new Date().toISOString(),
updated_at: privateData.updated_at || new Date().toISOString(),
notes: privateData.notes || []
};
folderData.push(folder);
});
// Manage loading state
if (hasFoldersToLoad && !hasAllPrivateData) {
setLoadingFolders(true);
} else if (hasAllPrivateData) {
setLoadingFolders(false);
setFolders(folderData);
// Update stats
setStats({
total: folderData.length
});
}
}, [folderProcesses, myFolderProcesses, folderPrivateData, fetchFolderPrivateData]);
// 4NK Integration useEffects
useEffect(() => {
const userStore = UserStore.getInstance();
const connected = userStore.isConnected();
const pairingId = userStore.getUserPairingId();
console.log('Initial 4NK state:', { connected, pairingId });
setIsConnected(connected);
setUserPairingId(pairingId);
}, []);
useEffect(() => {
const handleConnectionFlow = async () => {
if (!isConnected) return;
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const userStore = UserStore.getInstance();
let pairingId = userStore.getUserPairingId();
// 1⃣ Créer ou récupérer le pairing
if (!pairingId) {
pairingId = await messageBus.createUserPairing();
console.log("✅ Pairing created:", pairingId);
if (pairingId) {
userStore.pair(pairingId);
setUserPairingId(pairingId);
}
} else {
console.log("🔗 Already paired with ID:", pairingId);
}
// 2⃣ Charger les processes
const processes = await messageBus.getProcesses();
setProcesses(processes);
setFolderProcesses(processes);
// 3⃣ Charger les myProcesses
const myProcesses = await messageBus.getMyProcesses();
setMyProcesses(myProcesses);
setMyFolderProcesses(myProcesses);
} catch (err) {
console.error("❌ Error during pairing or process loading:", err);
}
};
handleConnectionFlow();
}, [isConnected, iframeUrl]);
// Load folders from 4NK when folder processes are available
useEffect(() => {
if (folderProcesses && myFolderProcesses.length >= 0) {
loadFoldersFrom4NK();
}
}, [folderProcesses, myFolderProcesses, loadFoldersFrom4NK]);
// Update folders when private data changes
useEffect(() => {
if (folderProcesses && Object.keys(folderPrivateData).length > 0) {
loadFoldersFrom4NK();
}
}, [folderPrivateData, loadFoldersFrom4NK, folderProcesses]);
// Filter and sort folders
const filteredFolders = folders.filter(folder => {
const matchesSearch = folder.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
folder.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
folder.folderNumber.toLowerCase().includes(searchTerm.toLowerCase())
return matchesSearch
})
const sortedFolders = [...filteredFolders].sort((a, b) => {
let aValue: any, bValue: any
switch (sortBy) {
case "name":
aValue = a.name.toLowerCase()
bValue = b.name.toLowerCase()
break
case "created_at":
aValue = new Date(a.created_at)
bValue = new Date(b.created_at)
break
case "updated_at":
aValue = new Date(a.updated_at)
bValue = new Date(b.updated_at)
break
default:
aValue = a.name.toLowerCase()
bValue = b.name.toLowerCase()
}
if (sortOrder === "asc") {
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
} else {
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0
}
})
// Modal handlers
const handleOpenModal = (type: FolderType) => {
setFolderType(type);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setFolderType(null);
};
const handleSaveNewFolder = useCallback(
(folderData: FolderData) => {
if (!isConnected || !userPairingId) {
console.error('Conditions non remplies:', { isConnected, userPairingId });
showNotification(
"error",
`Vous devez être connecté à 4NK pour créer un dossier (Connected: ${isConnected}, PairingId: ${userPairingId ? 'OK' : 'NULL'})`
);
return;
}
const roles = setDefaultFolderRoles(userPairingId);
const folderPrivateFields = FolderPrivateFields;
MessageBus.getInstance(iframeUrl)
.createFolder(folderData, folderPrivateFields, roles)
.then((_folderCreated: FolderCreated) => {
const firstStateId = _folderCreated.process.states[0].state_id;
MessageBus.getInstance(iframeUrl)
.notifyProcessUpdate(_folderCreated.processId, firstStateId)
.then(async () => {
// Recharger les processes et myProcesses
const messageBus = MessageBus.getInstance(iframeUrl);
const [processes, myProcesses] = await Promise.all([
messageBus.getProcesses(),
messageBus.getMyProcesses()
]);
setProcesses(processes);
setFolderProcesses(processes);
setMyProcesses(myProcesses);
setMyFolderProcesses(myProcesses);
showNotification("success", "Dossier créé avec succès !");
handleCloseModal();
});
})
.catch((error: any) => {
console.error('Erreur lors de la création du dossier:', error);
showNotification("error", "Erreur lors de la création du dossier");
});
},
[isConnected, userPairingId, loadFoldersFrom4NK]
);
// Auth connection handler
const handleAuthConnect = useCallback(() => {
setIsConnected(true);
setShowAuthModal(false);
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
showNotification("success", "Connexion 4NK réussie");
}, []);
const handleAuthClose = useCallback(() => {
setShowAuthModal(false);
}, []);
return (
<div className="flex h-screen bg-gray-900">
{/* Sidebar */}
<div className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
<div className="p-6 border-b border-gray-700">
<h1 className="text-xl font-semibold text-gray-100">Dossiers</h1>
<p className="text-sm text-gray-400 mt-1">Gérez vos dossiers 4NK</p>
</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-100">Dossiers</h1>
<p className="text-gray-400 mt-1">Organisez vos documents par dossiers</p>
</div>
<div className="flex items-center space-x-3 mt-4 sm:mt-0">
<Button variant="outline" size="sm">
<Upload className="h-4 w-4 mr-2 text-gray-100" />
Importer
</Button>
{isConnected ? (
<div className="flex gap-2">
{/* Nouveau dossier avec menu */}
<div className="relative">
<Button size="sm" onClick={() => setMenuOpen(!menuOpen)}>
<FolderPlus className="h-4 w-4 mr-2" />
Nouveau dossier
</Button>
{menuOpen && (
<div className="absolute mt-1 right-0 w-48 bg-white border border-gray-200 rounded shadow-lg z-50">
<button
className="w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100"
onClick={() => handleOpenModal("general")}
>
Nouveau dossier
</button>
</div>
)}
</div>
{/* Déconnexion */}
<Button variant="outline" size="sm" onClick={handleLogout}>
<X className="h-4 w-4 mr-2" />
Déconnexion 4NK
</Button>
</div>
) : (
<Button variant="outline" size="sm" onClick={handleLogin}>
<Shield className="h-4 w-4 mr-2" />
Connexion 4NK
</Button>
)}
</div>
</div>
{/* Breadcrumb */}
<div className="flex items-center space-x-2 text-sm text-gray-400">
{currentPath.map((path, index) => (
<div key={index} className="flex items-center space-x-2">
{index > 0 && <ChevronRight className="h-4 w-4 text-gray-400" />}
<button
className={`hover:text-gray-100 ${index === currentPath.length - 1 ? "font-medium text-gray-100" : ""}`}
onClick={() => setCurrentPath(currentPath.slice(0, index + 1))}
>
{path}
</button>
</div>
))}
</div>
{/* Search and Filters */}
<Card className="bg-gray-900 border-gray-700 text-gray-100">
<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-700 text-white border-blue-600" : ""}
>
<Filter className="h-4 w-4 mr-2 text-gray-100" />
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 text-gray-300">
Trier par:
</Label>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-32 bg-gray-800 text-gray-100 border-gray-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-800 text-gray-100 border-gray-700">
<SelectItem value="modified">Modifié</SelectItem>
<SelectItem value="name">Nom</SelectItem>
<SelectItem value="size">Taille</SelectItem>
<SelectItem value="owner">Propriétaire</SelectItem>
<SelectItem value="documents">Documents</SelectItem>
</SelectContent>
</Select>
<Button variant="ghost" size="sm" onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}>
{sortOrder === "asc" ? <SortAsc className="h-4 w-4 text-gray-100" /> : <SortDesc className="h-4 w-4 text-gray-100" />}
</Button>
</div>
</div>
</div>
{/* Advanced Filters */}
{showFilters && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<Label htmlFor="filterAccess" className="text-sm font-medium text-gray-300">
Accès
</Label>
<Select value={filterAccess} onValueChange={setFilterAccess}>
<SelectTrigger className="bg-gray-800 text-gray-100 border-gray-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-800 text-gray-100 border-gray-700">
<SelectItem value="all">Tous les accès</SelectItem>
<SelectItem value="shared">Partagés</SelectItem>
<SelectItem value="private">Privés</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="filterOwner" className="text-sm font-medium text-gray-300">
Propriétaire
</Label>
<Select value={filterOwner} onValueChange={setFilterOwner}>
<SelectTrigger className="bg-gray-800 text-gray-100 border-gray-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-800 text-gray-100 border-gray-700">
<SelectItem value="all">Tous les propriétaires</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 text-gray-300">
Type de stockage
</Label>
<Select value={filterStorage} onValueChange={setFilterStorage}>
<SelectTrigger className="bg-gray-800 text-gray-100 border-gray-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-800 text-gray-100 border-gray-700">
<SelectItem value="all">Tous les stockages</SelectItem>
<SelectItem value="temporary">
<div className="flex items-center space-x-2">
<HardDrive className="h-4 w-4 text-gray-100" />
<span>Temporaire</span>
</div>
</SelectItem>
<SelectItem value="permanent">
<div className="flex items-center space-x-2">
<Cloud className="h-4 w-4 text-gray-100" />
<span>Permanent</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button
variant="outline"
onClick={() => {
setFilterAccess("all")
setFilterOwner("all")
setFilterStorage("all")
setSearchTerm("")
}}
>
Réinitialiser
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
{/* Bulk Actions minimalistes: certificats et rôles uniquement */}
{selectedFolders.length > 0 && (
<Card className="bg-gray-800 border-gray-700 text-gray-100">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Checkbox
checked={selectedFolders.length === filteredFolders.length}
onCheckedChange={selectAllFolders}
/>
<span className="text-sm font-medium">
{selectedFolders.length} dossier{selectedFolders.length > 1 ? "s" : ""} sélectionné
{selectedFolders.length > 1 ? "s" : ""}
</span>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
>
<Download className="h-4 w-4 mr-2 text-gray-100" />
Télécharger
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const selected = folders.filter((f) => selectedFolders.includes(f.id))
selected.forEach((folder) => {
setActionModal({ type: "certificate", folder, folders: [] })
})
}}
>
<CheckCircle className="h-4 w-4 mr-2 text-gray-100" />
Valider
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
showNotification("info", "Déplacement groupé de dossiers (mock)")
}}
>
<FolderOpen className="h-4 w-4 mr-2 text-gray-100" />
Déplacer
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const selected = folders.filter((f) => selectedFolders.includes(f.id))
selected.forEach((folder) => {
setActionModal({ type: "storage_config", folder, folders: [] })
})
}}
>
<HardDrive className="h-4 w-4 mr-2 text-gray-100" />
Conservation
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const selected = folders.filter((f) => selectedFolders.includes(f.id))
const withCerts = selected.filter((f) => f.documents && f.documents.some((d) => d.hasCertificate))
if (withCerts.length === 0) {
setNotification({ type: "info", message: "Aucun certificat à télécharger pour la sélection" })
return
}
withCerts.forEach((f) => handleViewDocumentsCertificates(f))
}}
>
<ShieldCheck className="h-4 w-4 mr-2 text-gray-100" />
Certificats
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const first = folders.find((f) => selectedFolders.includes(f.id))
if (first) {
handleManageRoles(first)
} else {
setNotification({ type: "info", message: "Sélectionnez au moins un dossier" })
}
}}
>
<Users className="h-4 w-4 mr-2 text-gray-100" />
Rôles
</Button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Folders List/Grid */}
<Card>
<CardContent className="p-0">
{viewMode === "list" ? (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-800">
<tr>
<th className="text-left py-3 px-4 w-8">
<Checkbox
checked={selectedFolders.length === filteredFolders.length}
onCheckedChange={selectAllFolders}
/>
</th>
<th className="text-left py-3 px-4 font-medium text-gray-100">Nom</th>
<th className="text-left py-3 px-4 font-medium text-gray-100">Taille</th>
<th className="text-left py-3 px-4 font-medium text-gray-100">Modifié</th>
<th className="text-left py-3 px-4 font-medium text-gray-100">Propriétaire</th>
<th className="text-left py-3 px-4 font-medium text-gray-100">Accès</th>
<th className="text-left py-3 px-4 font-medium text-gray-100">Statut</th>
</tr>
</thead>
<tbody>
{filteredFolders.map((folder) => (
<tr
key={folder.id}
className="border-b border-gray-700 hover:bg-gray-800"
>
<td className="py-3 px-4">
<Checkbox
checked={selectedFolders.includes(folder.id)}
onCheckedChange={() => toggleFolderSelection(folder.id)}
/>
</td>
<td className="py-3 px-4">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${getFolderColor(folder.color)}`}>
<Folder className="h-5 w-5 text-black" />
</div>
<div>
<div className="flex items-center space-x-2">
<span
className="font-medium text-gray-100 cursor-pointer hover:underline"
onClick={() => handleOpenFolder(folder)}
>
{folder.name}
</span>
{isNewFolder(folder) && (
<Badge className="bg-blue-700 text-blue-100 border-blue-600">NEW</Badge>
)}
{getStorageIcon(folder.storageType)}
{folder.access === "private" && (
<Lock className="h-4 w-4 text-gray-400" />
)}
</div>
<p className="text-sm text-gray-400 truncate max-w-xs">{folder.description}</p>
</div>
</div>
</td>
<td className="py-3 px-4 text-gray-400">{folder.size}</td>
<td className="py-3 px-4 text-gray-400">{formatDate(folder.modified)}</td>
<td className="py-3 px-4 text-gray-400">{folder.owner}</td>
<td className="py-3 px-4">
<Badge
variant="outline"
className={
folder.access === "shared"
? "bg-green-700 text-green-100 border-green-600"
: "bg-orange-700 text-orange-100 border-orange-600"
}
>
{folder.access === "shared" ? "Partagé" : "Privé"}
</Badge>
</td>
<td className="py-3 px-4">{getStatusBadge(folder.status)}</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 gap-6">
{filteredFolders.map((folder) => (
<div
key={folder.id}
className={`relative group border rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer ${selectedFolders.includes(folder.id)
? "bg-blue-900 border-blue-700"
: "bg-gray-800 border-gray-700"
}`}
onClick={() => handleOpenFolder(folder)}
>
<div className="absolute top-4 left-4" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={selectedFolders.includes(folder.id)}
onCheckedChange={() => toggleFolderSelection(folder.id)}
/>
</div>
<div
className="absolute top-4 right-4 flex items-center space-x-1"
onClick={(e) => e.stopPropagation()}
>
{isNewFolder(folder) && (
<Badge className="bg-blue-700 text-blue-100 border-blue-600">NEW</Badge>
)}
{folder.access === "private" && <Lock className="h-4 w-4 text-gray-400" />}
{folder.storageType === "temporary" && (
<Button
variant="ghost"
size="sm"
onClick={() => handleStorageConfig(folder)}
className="h-8 w-8 p-0"
title="Configurer le stockage temporaire"
>
<Timer className="h-4 w-4 text-gray-100" />
</Button>
)}
{folder.status === "validated" && (
<Button
variant="ghost"
size="sm"
onClick={() => handleDownloadCertificate(folder)}
className="h-8 w-8 p-0"
title="Télécharger le certificat blockchain"
>
<ShieldCheck className="h-4 w-4 text-green-400" />
</Button>
)}
{folder.documents && folder.documents.some((doc) => doc.hasCertificate) && (
<Button
variant="ghost"
size="sm"
onClick={() => handleViewDocumentsCertificates(folder)}
className="h-8 w-8 p-0"
title="Certificats des documents"
>
<FileCheck className="h-4 w-4 text-blue-400" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleManageRoles(folder)}
className="h-8 w-8 p-0"
title="Gérer les rôles"
>
<Users className="h-4 w-4 text-gray-100" />
</Button>
</div>
<div className="flex flex-col items-center space-y-4 mt-8">
<div className={`p-4 rounded-xl ${getFolderColor(folder.color)}`}>
<Folder className="h-12 w-12 text-gray-100" />
</div>
<div className="text-center space-y-2 w-full">
<h3
className="font-semibold text-gray-100 text-lg truncate"
title={folder.name}
>
{folder.name}
</h3>
<p className="text-sm text-gray-400 line-clamp-2">{folder.description}</p>
<div className="text-xs text-gray-400">
<p>{folder.size}</p>
<p>{formatDate(folder.modified)}</p>
<div className="flex items-center justify-center space-x-1 mt-1">
{getStorageIcon(folder.storageType)}
<span>{folder.storageType === "permanent" ? "Permanent" : "Temporaire"}</span>
</div>
{folder.temporaryStorageConfig && folder.storageType === "temporary" && (
<div className="text-xs text-blue-400 mt-1">
Durée: {folder.temporaryStorageConfig.duration} jours
</div>
)}
</div>
<div className="flex justify-center">{getStatusBadge(folder.status)}</div>
<Badge
variant="outline"
className={
folder.access === "shared"
? "bg-green-700 text-green-100 border-green-600"
: "bg-orange-700 text-orange-100 border-orange-600"
}
>
{folder.access === "shared" ? "Partagé" : "Privé"}
</Badge>
</div>
</div>
{/* Recent Activity */}
<div className="mt-4 pt-4 border-t border-gray-700">
<h4 className="text-xs font-medium text-gray-300 mb-2">Activité récente</h4>
<div className="space-y-1">
{folder.activity.slice(0, 2).map((activity, index) => (
<div key={index} className="text-xs text-gray-400">
<span className="font-medium">{activity.user}</span> a {activity.action}{" "}
<span className="font-medium">{activity.item}</span>
<div className="text-gray-500">{activity.time}</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
</div>
)}
{loadingFolders && isConnected && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<h3 className="text-lg font-medium text-gray-100 mb-2">Chargement des dossiers...</h3>
<p className="text-gray-400">Récupération des données privées depuis 4NK</p>
</div>
)}
{!loadingFolders && filteredFolders.length === 0 && (
<div className="text-center py-12">
<Folder className="h-12 w-12 mx-auto text-gray-500 mb-4" />
<h3 className="text-lg font-medium text-gray-100 mb-2">Aucun dossier trouvé</h3>
<p className="text-gray-400 mb-4">
{searchTerm || filterAccess !== "all" || filterOwner !== "all" || filterStorage !== "all"
? "Essayez de modifier vos critères de recherche"
: "Commencez par créer votre premier dossier"}
</p>
</div>
)}
</CardContent>
</Card>
{/* ProcessesViewer Card */}
<Card className="mt-6 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700">
<CardContent className="p-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Processus Blockchain</h3>
{/* Intégration du ProcessesViewer */}
<div className="w-full h-[500px]">
<ProcessesViewer
processes={processes}
myProcesses={myProcesses}
onProcessesUpdate={setProcesses}
/>
</div>
</CardContent>
</Card>
{/* Modals */}
{actionModal.type && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-900 text-gray-100 rounded-lg p-6 w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
{/* Documents Certificates Modal */}
{actionModal.type === "documents_certificates" && actionModal.folder && (
<>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-100">
Certificats des documents - {actionModal.folder.name}
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setActionModal({ type: null, folder: null, folders: [] })}
>
<X className="h-4 w-4 text-gray-100" />
</Button>
</div>
<div className="space-y-6">
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div className="flex items-center space-x-3">
<FileCheck className="h-8 w-8 text-blue-400" />
<div>
<h4 className="font-semibold text-blue-200">Certificats des documents</h4>
<p className="text-sm text-blue-300">
Téléchargez les certificats blockchain individuels des documents de ce dossier
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
{actionModal.folder.documents?.map((doc) => (
<div key={doc.id} className="border border-gray-700 rounded-lg p-4 bg-gray-900">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<FileText className="h-8 w-8 text-gray-300" />
<div>
<h5 className="font-medium text-gray-100">{doc.name}</h5>
{doc.hasCertificate && doc.certificateId && (
<p className="text-sm text-gray-400">ID: {doc.certificateId}</p>
)}
</div>
</div>
<div className="flex items-center space-x-2">
{doc.hasCertificate ? (
<>
<Badge className="bg-green-800 text-green-100">Certifié</Badge>
<Button
variant="outline"
size="sm"
onClick={() => {
showNotification("info", `Téléchargement du certificat pour ${doc.name}...`)
setTimeout(() => {
showNotification("success", `Certificat de ${doc.name} téléchargé`)
sendFolderChatNotification(
actionModal.folder!.id.toString(),
`📜 Certificat du document "${doc.name}" téléchargé`,
"document_certificate_download",
)
}, 1500)
}}
>
<Download className="h-4 w-4 mr-2" />
Télécharger
</Button>
</>
) : (
<Badge variant="outline" className="bg-gray-700 text-gray-300">
Non certifié
</Badge>
)}
</div>
</div>
</div>
))}
</div>
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
<h5 className="font-medium text-gray-100 mb-3">Actions groupées</h5>
<div className="flex space-x-3">
<Button
variant="outline"
onClick={() => {
const certifiedDocs = actionModal.folder!.documents?.filter((doc) => doc.hasCertificate) || []
showNotification("info", `Téléchargement de ${certifiedDocs.length} certificat(s)...`)
setTimeout(() => {
showNotification("success", `${certifiedDocs.length} certificat(s) téléchargé(s)`)
sendFolderChatNotification(
actionModal.folder!.id.toString(),
`📦 Archive des certificats téléchargée (${certifiedDocs.length} documents)`,
"bulk_certificates_download",
)
}, 2000)
}}
>
<Archive className="h-4 w-4 mr-2" />
Télécharger tous les certificats (.zip)
</Button>
<Button
variant="outline"
onClick={() => {
showNotification("info", "Vérification en ligne des certificats...")
setTimeout(() => {
showNotification("success", "Tous les certificats sont valides")
}, 3000)
}}
>
<ShieldCheck className="h-4 w-4 mr-2" />
Vérifier tous en ligne
</Button>
</div>
</div>
</div>
</>
)}
{/* Storage Config Modal */}
{actionModal.type === "storage_config" && actionModal.folder && (
<>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-100">
Configuration du stockage temporaire
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setActionModal({ type: null, folder: null, folders: [] })}
>
<X className="h-4 w-4 text-gray-100" />
</Button>
</div>
<div className="space-y-4">
<div className="bg-gray-800 p-3 rounded-lg border border-gray-700">
<div className="flex items-center space-x-2 mb-2">
<Timer className="h-5 w-5 text-orange-400" />
<span className="font-medium text-orange-300">
Configuration du stockage temporaire
</span>
</div>
<p className="text-sm text-orange-200">
Configurez la durée de conservation et les informations d'usage pour le dossier{" "}
<strong>{actionModal.folder.name}</strong> en stockage temporaire.
</p>
</div>
<div>
<Label htmlFor="storageDuration" className="text-gray-100">Durée de conservation (en jours)</Label>
<Select value={storageDuration} onValueChange={setStorageDuration}>
<SelectTrigger className="bg-gray-900 text-gray-100 border-gray-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-900 text-gray-100 border-gray-700">
<SelectItem value="7">7 jours</SelectItem>
<SelectItem value="15">15 jours</SelectItem>
<SelectItem value="30">30 jours</SelectItem>
<SelectItem value="60">60 jours</SelectItem>
<SelectItem value="90">90 jours</SelectItem>
<SelectItem value="180">180 jours</SelectItem>
<SelectItem value="365">1 an</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-400 mt-1">
Durée pendant laquelle les données seront conservées en stockage temporaire avant archivage automatique
</p>
</div>
<div>
<Label htmlFor="dataUsage" className="text-gray-100">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 ces données (ex: analyses commerciales, rapports internes, documentation projet...)"
rows={3}
className="bg-gray-900 text-gray-100 border-gray-700"
/>
<p className="text-xs text-gray-400 mt-1">
Description de l'utilisation prévue des données contenues dans ce dossier
</p>
</div>
<div>
<Label htmlFor="thirdPartyAccess" className="text-gray-100">Tiers pouvant avoir accès</Label>
<Textarea
id="thirdPartyAccess"
value={thirdPartyAccess}
onChange={(e) => setThirdPartyAccess(e.target.value)}
placeholder="Listez les tiers externes qui pourraient avoir accès à ces données (ex: consultants, partenaires, auditeurs...)"
rows={3}
className="bg-gray-900 text-gray-100 border-gray-700"
/>
<p className="text-xs text-gray-400 mt-1">
Liste des parties externes qui pourraient être amenées à consulter ces données
</p>
</div>
<div className="bg-gray-800 p-3 rounded-lg border border-gray-700">
<div className="flex items-center space-x-2 mb-2">
<Info className="h-4 w-4 text-blue-400" />
<span className="font-medium text-blue-200">Information RGPD</span>
</div>
<p className="text-xs text-blue-300">
Ces informations sont utilisées pour assurer la conformité RGPD et la traçabilité des données.
Elles seront incluses dans le registre des traitements.
</p>
</div>
<div className="flex justify-end space-x-2">
<Button
variant="outline"
onClick={() => setActionModal({ type: null, folder: null, folders: [] })}
>
Annuler
</Button>
<Button onClick={confirmStorageConfig}>
<Timer className="h-4 w-4 mr-2" />
Enregistrer la configuration
</Button>
</div>
</div>
</>
)}
{/* Certificate Modal */}
{actionModal.type === "certificate" && actionModal.folder && (
<>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-100">
Certificat de validation du dossier
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setActionModal({ type: null, folder: null, folders: [] })}
>
<X className="h-4 w-4 text-gray-100" />
</Button>
</div>
<div className="space-y-6">
<div className="bg-gray-800 border border-green-700 rounded-lg p-4">
<div className="flex items-center space-x-3">
<ShieldCheck className="h-8 w-8 text-green-400" />
<div>
<h4 className="font-semibold text-green-300">Dossier certifié</h4>
<p className="text-sm text-green-200">
Ce dossier et tous ses documents ont été validés et certifiés numériquement
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-gray-900 p-4 rounded-lg border border-gray-700">
<h5 className="font-medium text-gray-100 mb-3">Informations du dossier</h5>
<div className="space-y-2 text-sm text-gray-300">
<div className="flex justify-between">
<span className="text-gray-400">Nom :</span>
<span className="font-medium">{actionModal.folder.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Documents :</span>
<span className="font-medium">{actionModal.folder.documentsCount}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Taille totale :</span>
<span className="font-medium">{actionModal.folder.size}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Type :</span>
<span className="font-medium capitalize">{actionModal.folder.type}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Hash du dossier :</span>
<span className="font-mono text-xs bg-gray-800 p-1 rounded break-all text-gray-200">
{Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)}
</span>
</div>
</div>
</div>
<div className="bg-gray-900 p-4 rounded-lg border border-gray-700">
<h5 className="font-medium text-gray-100 mb-3">Certificat numérique</h5>
<div className="space-y-2 text-sm text-gray-300">
<div className="flex justify-between">
<span className="text-gray-400">Émis le :</span>
<span className="font-medium">{actionModal.folder.modified.toLocaleDateString("fr-FR")}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Validé par :</span>
<span className="font-medium">{actionModal.folder.owner}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Autorité :</span>
<span className="font-medium">DocV Folder Certification</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">ID Certificat :</span>
<span className="font-mono text-xs bg-gray-800 p-1 rounded text-gray-200">
FOLDER-CERT-{actionModal.folder.id}-{new Date().getFullYear()}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Validité :</span>
<span className="font-medium text-green-400">
{new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toLocaleDateString("fr-FR")}
</span>
</div>
</div>
</div>
</div>
<div className="bg-gray-800 border border-blue-700 rounded-lg p-4">
<h5 className="font-medium text-blue-300 mb-3">Validation du dossier complet</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-gray-300">
<div className="space-y-2 text-sm">
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Intégrité de tous les documents vérifiée</span>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Structure du dossier validée</span>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Permissions et accès contrôlés</span>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Horodatage certifié pour tous les fichiers</span>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Conformité RGPD du dossier</span>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Traçabilité complète des modifications</span>
</div>
</div>
</div>
</div>
<div className="bg-gray-900 border border-green-700 rounded-lg p-4">
<h5 className="font-medium text-gray-100 mb-3">Chaîne de confiance distribuée</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-gray-300">
<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>Block #{Math.floor(Math.random() * 1000000)} - Dossier principal</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span>{actionModal.folder.documentsCount} documents liés dans la blockchain</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
<span>Réplication sur {Math.floor(Math.random() * 5) + 3} nœuds souverains</span>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
<span>Stockage {actionModal.folder.storageType === "permanent" ? "permanent" : "temporaire"} certifié</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<span>{Math.floor(Math.random() * 100) + 50} confirmations réseau</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-indigo-500 rounded-full"></div>
<span>Audit de sécurité: {new Date().toLocaleDateString("fr-FR")}</span>
</div>
</div>
</div>
</div>
{actionModal.folder.expectedDocuments.length > 0 && (
<div className="bg-gray-800 border border-yellow-700 rounded-lg p-4">
<h5 className="font-medium text-yellow-300 mb-3">Documents attendus - Statut de validation</h5>
<div className="space-y-2 text-gray-300 text-sm">
{actionModal.folder.expectedDocuments.map((doc, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FileText className="h-4 w-4 text-gray-400" />
<span>{doc.name}</span>
{doc.required && <span className="text-red-500 text-xs">*</span>}
</div>
<div className="flex items-center space-x-2">
<Badge
className={
doc.status === "received"
? "bg-green-700 text-green-200"
: doc.status === "pending"
? "bg-orange-700 text-orange-200"
: "bg-red-700 text-red-200"
}
>
{doc.status === "received"
? " Validé"
: doc.status === "pending"
? " En attente"
: " Manquant"}
</Badge>
</div>
</div>
))}
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-700">
<Button
variant="outline"
onClick={() => {
showNotification("success", `Certificat du dossier ${actionModal.folder!.name} téléchargé`);
sendFolderChatNotification(
actionModal.folder!.id.toString(),
`📜 Certificat de validation du dossier téléchargé`,
"folder_certificate_download",
);
}}
>
<Download className="h-4 w-4 mr-2" />
Télécharger le certificat (.pdf)
</Button>
<Button
variant="outline"
onClick={() => {
showNotification("info", "Vérification en ligne du certificat du dossier...");
setTimeout(() => {
showNotification("success", "Certificat du dossier vérifié avec succès");
}, 3000);
}}
>
<ShieldCheck className="h-4 w-4 mr-2" />
Vérifier en ligne
</Button>
<Button
variant="outline"
onClick={() => {
showNotification("info", "Préparation de l'archive certifiée...");
setTimeout(() => {
showNotification(
"success",
`Archive certifiée du dossier ${actionModal.folder!.name} téléchargée`,
);
sendFolderChatNotification(
actionModal.folder!.id.toString(),
`📦 Archive certifiée complète téléchargée (${actionModal.folder!.documentsCount} documents)`,
"certified_archive_download",
);
}, 4000);
}}
>
<Archive className="h-4 w-4 mr-2" />
Archive certifiée (.zip)
</Button>
</div>
</div>
</>
)}
{/* Request Document Modal */}
{actionModal.type === "request_document" && actionModal.folder && (
<>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-100">Demander un document</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setActionModal({ type: null, folder: null, folders: [] })}
>
<X className="h-4 w-4 text-gray-100" />
</Button>
</div>
<div className="space-y-4">
<div className="bg-gray-800 p-3 rounded-lg">
<p className="text-sm text-blue-300">
Sélectionnez un document attendu pour le dossier <strong>{actionModal.folder.name}</strong> et
envoyez une demande à la personne responsable.
</p>
</div>
<div>
<Label htmlFor="selectedDocument" className="text-gray-100">Document à demander</Label>
<Select value={selectedDocument} onValueChange={setSelectedDocument}>
<SelectTrigger className="bg-gray-900 text-gray-100 border-gray-700">
<SelectValue placeholder="Choisir un document" />
</SelectTrigger>
<SelectContent className="bg-gray-900 text-gray-100 border-gray-700">
{actionModal.folder.expectedDocuments.map((doc) => (
<SelectItem key={doc.name} value={doc.name}>
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-2">
<FileText className="h-4 w-4 text-gray-300" />
<span>{doc.name}</span>
{doc.required && <span className="text-red-500">*</span>}
</div>
<div className="flex items-center space-x-2">
<Badge
className={
doc.status === "received"
? "bg-green-700 text-green-200"
: doc.status === "pending"
? "bg-orange-700 text-orange-200"
: "bg-red-700 text-red-200"
}
>
{doc.status === "received"
? "Reçu"
: doc.status === "pending"
? "En attente"
: "Manquant"}
</Badge>
<span className="text-xs text-gray-400">({doc.assignedRole})</span>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="requestMessage" className="text-gray-100">Message de demande</Label>
<Textarea
id="requestMessage"
value={requestMessage}
onChange={(e) => setRequestMessage(e.target.value)}
placeholder="Ajouter un message pour expliquer votre demande..."
rows={3}
className="bg-gray-900 text-gray-100 border-gray-700 placeholder-gray-500"
/>
</div>
<div className="flex justify-end space-x-2">
<Button
variant="outline"
onClick={() => setActionModal({ type: null, folder: null, folders: [] })}
>
Annuler
</Button>
<Button onClick={confirmRequestDocument} disabled={!selectedDocument}>
<FileQuestion className="h-4 w-4 mr-2" />
Envoyer la demande
</Button>
</div>
</div>
</>
)}
</div>
</div>
)}
{/* Modals */}
{isModalOpen && (
<FolderModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onSave={handleSaveNewFolder}
onCancel={handleCloseModal}
folderType={folderType || "autre"}
/>
)}
{showAuthModal && (
<AuthModal
isOpen={showAuthModal}
onClose={handleAuthClose}
onConnect={handleAuthConnect}
iframeUrl={iframeUrl}
/>
)}
{/* Notification */}
{notification && (
<div className="fixed top-4 right-4 z-50">
<div className={`p-4 rounded-md shadow-lg ${
notification.type === "success" ? "bg-green-50 text-green-800 border border-green-200" :
notification.type === "error" ? "bg-red-50 text-red-800 border border-red-200" :
"bg-blue-50 text-blue-800 border border-blue-200"
}`}>
<div className="flex items-center justify-between">
<span>{notification.message}</span>
<Button
variant="ghost"
size="sm"
onClick={() => setNotification(null)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
{/* 4NK Iframe - only show when connected */}
{isConnected && <Iframe iframeUrl={iframeUrl} />}
</div>
)
}