960 lines
39 KiB
TypeScript
960 lines
39 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="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>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{/* Folder list */}
|
||
<div className="space-y-2">
|
||
{sortedFolders.map((folder) => (
|
||
<Card key={folder.folderNumber} className="hover:shadow-md transition-shadow bg-gray-800 border border-gray-700">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-3">
|
||
<Folder className="h-5 w-5 text-blue-600" />
|
||
<div>
|
||
<h3 className="font-medium text-gray-100">{folder.name}</h3>
|
||
<p className="text-sm text-gray-400">{folder.description}</p>
|
||
<div className="flex items-center space-x-4 mt-1">
|
||
<span className="text-xs text-gray-400">#{folder.folderNumber}</span>
|
||
<span className="text-xs text-gray-400">
|
||
<Clock className="h-3 w-3 inline mr-1" />
|
||
{new Date(folder.updated_at).toLocaleDateString()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
{folder.notes.length > 0 && (
|
||
<Badge variant="outline">{folder.notes.length} notes</Badge>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</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>
|
||
)
|
||
}
|