1455 lines
66 KiB
TypeScript
1455 lines
66 KiB
TypeScript
"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="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-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}
|
||
/>
|
||
)}
|
||
|
||
{/* 4NK Iframe - only show when connected */}
|
||
{isConnected && <Iframe iframeUrl={iframeUrl} />}
|
||
</div>
|
||
)
|
||
}
|