2425 lines
102 KiB
TypeScript
2425 lines
102 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect, useCallback } from "react"
|
||
import { useRouter, useSearchParams } from "next/navigation"
|
||
import { Card, CardContent } from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Checkbox } from "@/components/ui/checkbox"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import {
|
||
Folder,
|
||
FolderOpen,
|
||
Grid3X3,
|
||
List,
|
||
Search,
|
||
Filter,
|
||
Share2,
|
||
Trash2,
|
||
Users,
|
||
FileText,
|
||
Clock,
|
||
Star,
|
||
ChevronRight,
|
||
FolderPlus,
|
||
Upload,
|
||
Download,
|
||
Lock,
|
||
SortAsc,
|
||
SortDesc,
|
||
X,
|
||
UserPlus,
|
||
Crown,
|
||
Shield,
|
||
User,
|
||
CheckCircle,
|
||
XCircle,
|
||
Info,
|
||
CloudUpload,
|
||
Cloud,
|
||
HardDrive,
|
||
FileQuestion,
|
||
Timer,
|
||
ShieldCheck,
|
||
Archive,
|
||
FileCheck,
|
||
} from "lucide-react"
|
||
import ProcessesViewer from "@/components/4nk/ProcessesViewer"
|
||
import { FolderData as SDKFolderData, 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 EventBus from "@/lib/4nk/EventBus"
|
||
import FolderModal from "@/components/4nk/FolderModal"
|
||
import AuthModal from "@/components/4nk/AuthModal"
|
||
import Iframe from "@/components/4nk/Iframe"
|
||
|
||
interface FolderData {
|
||
id: number
|
||
name: string
|
||
description: string
|
||
documentsCount: number
|
||
subfoldersCount: number
|
||
size: string
|
||
created: Date
|
||
modified: Date
|
||
owner: string
|
||
access: "shared" | "private"
|
||
members: string[]
|
||
tags: string[]
|
||
color: string
|
||
favorite: boolean
|
||
storageType: "temporary" | "permanent"
|
||
status: "active" | "archived" | "pending" | "completed" | "validated"
|
||
type: string
|
||
expectedDocuments: Array<{
|
||
name: string
|
||
required: boolean
|
||
assignedRole: "owner" | "editor" | "validator" | "contributor"
|
||
status: "missing" | "pending" | "received"
|
||
}>
|
||
activity: Array<{
|
||
user: string
|
||
action: string
|
||
item: string
|
||
time: string
|
||
}>
|
||
permissions: {
|
||
canView: boolean
|
||
canEdit: boolean
|
||
canDelete: boolean
|
||
canInvite: boolean
|
||
canArchive: boolean
|
||
canAnalyze: boolean
|
||
}
|
||
temporaryStorageConfig?: {
|
||
duration: number // en jours
|
||
dataUsage: string
|
||
thirdPartyAccess: string
|
||
}
|
||
documents?: Array<{
|
||
id: string
|
||
name: string
|
||
hasCertificate: boolean
|
||
certificateId?: string
|
||
}>
|
||
}
|
||
|
||
interface ActionModal {
|
||
type:
|
||
| "invite"
|
||
| "delete"
|
||
| "create"
|
||
| "edit"
|
||
| "archive"
|
||
| "request_document"
|
||
| "storage_config"
|
||
| "certificate"
|
||
| "documents_certificates"
|
||
| null
|
||
folder: FolderData | null
|
||
folders: FolderData[]
|
||
}
|
||
|
||
interface UserWithRoles {
|
||
id: string
|
||
name: string
|
||
email: string
|
||
avatar: string
|
||
folderRoles: {
|
||
[folderId: string]: {
|
||
role: "owner" | "editor" | "viewer" | "validator" | "contributor"
|
||
assignedDate: Date
|
||
}
|
||
}
|
||
spaceRole: "admin" | "manager" | "user" | "guest"
|
||
spaceRoles: {
|
||
[spaceId: string]: {
|
||
role: "admin" | "manager" | "user" | "guest"
|
||
spaceName: string
|
||
}
|
||
}
|
||
}
|
||
|
||
interface Role {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
level: "folder" | "space" | "global"
|
||
}
|
||
|
||
type FolderType = string;
|
||
|
||
export default function FoldersPage() {
|
||
const router = useRouter()
|
||
const searchParams = useSearchParams()
|
||
const [viewMode, setViewMode] = useState<'list'>('list')
|
||
const [searchTerm, setSearchTerm] = useState("")
|
||
const [selectedFolders, setSelectedFolders] = useState<number[]>([])
|
||
const [sortBy, setSortBy] = useState("modified")
|
||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
|
||
const [filterAccess, setFilterAccess] = useState("all")
|
||
const [filterOwner, setFilterOwner] = useState("all")
|
||
const [filterStorage, setFilterStorage] = useState("all")
|
||
const [showFilters, setShowFilters] = useState(false)
|
||
const [currentPath, setCurrentPath] = useState<string[]>(["Racine"])
|
||
const [actionModal, setActionModal] = useState<ActionModal>({ type: null, folder: null, folders: [] })
|
||
const [showCreateFolderModal, setShowCreateFolderModal] = useState(false)
|
||
const [folderType, setFolderType] = useState<FolderType | null>(null);
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [menuOpen, setMenuOpen] = 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 [inviteMessage, setInviteMessage] = useState("")
|
||
const [selectedUser, setSelectedUser] = useState("")
|
||
const [selectedRole, setSelectedRole] = useState("")
|
||
const [inviteScope, setInviteScope] = useState<"user" | "role">("user")
|
||
const [folderName, setFolderName] = useState("")
|
||
const [folderDescription, setFolderDescription] = useState("")
|
||
const [folderColor, setFolderColor] = useState("blue")
|
||
const [folderTags, setFolderTags] = useState("")
|
||
const [folderAccess, setFolderAccess] = useState<"shared" | "private">("private")
|
||
const [archiveReason, setArchiveReason] = useState("")
|
||
const [retentionPeriod, setRetentionPeriod] = useState("5")
|
||
const [selectedDocument, setSelectedDocument] = useState("")
|
||
const [requestMessage, setRequestMessage] = useState("")
|
||
const [notification, setNotification] = useState<{ type: "success" | "error" | "info"; message: string } | null>(null)
|
||
|
||
// Storage config modal states
|
||
const [storageDuration, setStorageDuration] = useState("30")
|
||
const [dataUsage, setDataUsage] = useState("")
|
||
const [thirdPartyAccess, setThirdPartyAccess] = useState("")
|
||
|
||
const [folders, setFolders] = useState<FolderData[]>([])
|
||
const [stats, setStats] = useState({
|
||
total: 0,
|
||
shared: 0,
|
||
private: 0,
|
||
thisWeek: 0,
|
||
permanent: 0,
|
||
temporary: 0,
|
||
})
|
||
|
||
const [users] = useState<UserWithRoles[]>([
|
||
{
|
||
id: "1",
|
||
name: "Marie Dubois",
|
||
email: "marie.dubois@company.com",
|
||
avatar: "MD",
|
||
folderRoles: {
|
||
"1": { role: "owner", assignedDate: new Date("2024-01-01") },
|
||
"4": { role: "editor", assignedDate: new Date("2024-01-05") },
|
||
},
|
||
spaceRole: "manager",
|
||
spaceRoles: {
|
||
main: { role: "manager", spaceName: "Espace Principal" },
|
||
legal: { role: "admin", spaceName: "Espace Juridique" },
|
||
},
|
||
},
|
||
{
|
||
id: "2",
|
||
name: "Sophie Laurent",
|
||
email: "sophie.laurent@company.com",
|
||
avatar: "SL",
|
||
folderRoles: {
|
||
"2": { role: "owner", assignedDate: new Date("2024-01-02") },
|
||
"3": { role: "contributor", assignedDate: new Date("2024-01-10") },
|
||
},
|
||
spaceRole: "user",
|
||
spaceRoles: {
|
||
main: { role: "user", spaceName: "Espace Principal" },
|
||
analytics: { role: "manager", spaceName: "Espace Analytics" },
|
||
},
|
||
},
|
||
{
|
||
id: "3",
|
||
name: "Jean Martin",
|
||
email: "jean.martin@company.com",
|
||
avatar: "JM",
|
||
folderRoles: {
|
||
"3": { role: "owner", assignedDate: new Date("2024-01-03") },
|
||
"2": { role: "viewer", assignedDate: new Date("2024-01-15") },
|
||
},
|
||
spaceRole: "user",
|
||
spaceRoles: {
|
||
main: { role: "user", spaceName: "Espace Principal" },
|
||
projects: { role: "admin", spaceName: "Espace Projets" },
|
||
},
|
||
},
|
||
{
|
||
id: "4",
|
||
name: "Pierre Durand",
|
||
email: "pierre.durand@company.com",
|
||
avatar: "PD",
|
||
folderRoles: {
|
||
"6": { role: "owner", assignedDate: new Date("2024-01-04") },
|
||
"5": { role: "validator", assignedDate: new Date("2024-01-08") },
|
||
},
|
||
spaceRole: "user",
|
||
spaceRoles: {
|
||
main: { role: "user", spaceName: "Espace Principal" },
|
||
training: { role: "admin", spaceName: "Espace Formation" },
|
||
},
|
||
},
|
||
{
|
||
id: "5",
|
||
name: "Admin Système",
|
||
email: "admin@company.com",
|
||
avatar: "AD",
|
||
folderRoles: {
|
||
"5": { role: "owner", assignedDate: new Date("2024-01-01") },
|
||
"1": { role: "validator", assignedDate: new Date("2024-01-01") },
|
||
"4": { role: "validator", assignedDate: new Date("2024-01-01") },
|
||
},
|
||
spaceRole: "admin",
|
||
spaceRoles: {
|
||
main: { role: "admin", spaceName: "Espace Principal" },
|
||
legal: { role: "admin", spaceName: "Espace Juridique" },
|
||
analytics: { role: "admin", spaceName: "Espace Analytics" },
|
||
projects: { role: "admin", spaceName: "Espace Projets" },
|
||
training: { role: "admin", spaceName: "Espace Formation" },
|
||
},
|
||
},
|
||
])
|
||
|
||
const [roles] = useState<Role[]>([
|
||
{
|
||
id: "folder-owner",
|
||
name: "Propriétaire du dossier",
|
||
description: "Contrôle total sur le dossier",
|
||
level: "folder",
|
||
},
|
||
{ id: "folder-editor", name: "Éditeur du dossier", description: "Peut modifier les documents", level: "folder" },
|
||
{
|
||
id: "folder-validator",
|
||
name: "Validateur du dossier",
|
||
description: "Peut valider les documents",
|
||
level: "folder",
|
||
},
|
||
{
|
||
id: "folder-contributor",
|
||
name: "Contributeur du dossier",
|
||
description: "Peut ajouter des documents",
|
||
level: "folder",
|
||
},
|
||
{ id: "folder-viewer", name: "Lecteur du dossier", description: "Lecture seule", level: "folder" },
|
||
{ id: "space-admin", name: "Administrateur d'espace", description: "Contrôle total sur l'espace", level: "space" },
|
||
{
|
||
id: "space-manager",
|
||
name: "Gestionnaire d'espace",
|
||
description: "Gestion des utilisateurs et dossiers",
|
||
level: "space",
|
||
},
|
||
{ id: "space-user", name: "Utilisateur d'espace", description: "Accès standard à l'espace", level: "space" },
|
||
{ id: "space-guest", name: "Invité d'espace", description: "Accès limité à l'espace", level: "space" },
|
||
{ id: "global-admin", name: "Administrateur global", description: "Accès à tous les espaces", level: "global" },
|
||
{ id: "global-manager", name: "Gestionnaire global", description: "Gestion multi-espaces", level: "global" },
|
||
])
|
||
|
||
const colors = [
|
||
{ id: "blue", name: "Bleu", class: "text-blue-600 bg-blue-100" },
|
||
{ id: "green", name: "Vert", class: "text-green-600 bg-green-100" },
|
||
{ id: "purple", name: "Violet", class: "text-purple-600 bg-purple-100" },
|
||
{ id: "orange", name: "Orange", class: "text-orange-600 bg-orange-100" },
|
||
{ id: "red", name: "Rouge", class: "text-red-600 bg-red-100" },
|
||
{ id: "pink", name: "Rose", class: "text-pink-600 bg-pink-100" },
|
||
{ id: "yellow", name: "Jaune", class: "text-yellow-600 bg-yellow-100" },
|
||
{ id: "gray", name: "Gris", class: "text-gray-600 bg-gray-100" },
|
||
]
|
||
|
||
// 4NK Integration useEffects
|
||
useEffect(() => {
|
||
const userStore = UserStore.getInstance();
|
||
const connected = userStore.isConnected();
|
||
const pairingId = userStore.getUserPairingId();
|
||
|
||
console.log('Initialisation 4NK:', { connected, pairingId });
|
||
|
||
setIsConnected(connected);
|
||
setUserPairingId(pairingId);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const handleConnectionFlow = async () => {
|
||
if (!isConnected) return;
|
||
|
||
const userStore = UserStore.getInstance();
|
||
const messageBus = MessageBus.getInstance(iframeUrl);
|
||
|
||
try {
|
||
await messageBus.isReady();
|
||
|
||
let pairingId = userStore.getUserPairingId();
|
||
|
||
// 1️⃣ Créer le pairing si non existant
|
||
if (!pairingId) {
|
||
console.log("🚀 No pairing found — creating new pairing...");
|
||
pairingId = await messageBus.createUserPairing();
|
||
console.log("✅ Pairing created:", 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); // Use same processes for folders
|
||
|
||
// 3️⃣ Charger les myProcesses
|
||
const myProcesses = await messageBus.getMyProcesses();
|
||
setMyProcesses(myProcesses);
|
||
setMyFolderProcesses(myProcesses); // Use same myProcesses for folders
|
||
|
||
} catch (err) {
|
||
console.error("❌ Error during pairing or process loading:", err);
|
||
}
|
||
};
|
||
|
||
handleConnectionFlow();
|
||
}, [isConnected, iframeUrl]);
|
||
|
||
// Notification system
|
||
const showNotification = (type: "success" | "error" | "info", message: string) => {
|
||
setNotification({ type, message })
|
||
setTimeout(() => setNotification(null), 3000)
|
||
}
|
||
|
||
// 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
|
||
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]; // -2 because last state is empty
|
||
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
|
||
}
|
||
|
||
// Only create folder when we have private data
|
||
const folder: FolderData = {
|
||
id: parseInt(folderNumber) || Math.floor(Math.random() * 10000),
|
||
name: privateData.name || `Dossier ${folderNumber}`,
|
||
description: privateData.description || '',
|
||
documentsCount: privateData.documentsCount || 0,
|
||
subfoldersCount: privateData.subfoldersCount || 0,
|
||
size: privateData.size || '0 MB',
|
||
created: new Date(privateData.created || Date.now()),
|
||
modified: new Date(privateData.modified || Date.now()),
|
||
owner: privateData.owner || 'Propriétaire 4NK',
|
||
access: (privateData.access || 'private') as 'shared' | 'private',
|
||
members: privateData.members || [],
|
||
tags: privateData.tags || [],
|
||
color: privateData.color || 'blue',
|
||
favorite: privateData.favorite || false,
|
||
storageType: (privateData.storageType || 'temporary') as 'temporary' | 'permanent',
|
||
status: (privateData.status || 'active') as 'active' | 'archived' | 'pending' | 'completed' | 'validated',
|
||
type: privateData.type || 'general',
|
||
expectedDocuments: privateData.expectedDocuments || [],
|
||
activity: privateData.activity || [],
|
||
permissions: privateData.permissions || {
|
||
canView: true,
|
||
canEdit: true, // User owns this process
|
||
canDelete: true, // User owns this process
|
||
canInvite: true, // User owns this process
|
||
canArchive: true, // User owns this process
|
||
canAnalyze: true,
|
||
},
|
||
temporaryStorageConfig: privateData.temporaryStorageConfig,
|
||
documents: privateData.documents || [],
|
||
};
|
||
|
||
folderData.push(folder);
|
||
});
|
||
|
||
// Manage loading state
|
||
if (hasFoldersToLoad && !hasAllPrivateData) {
|
||
setLoadingFolders(true);
|
||
} else if (hasAllPrivateData) {
|
||
setLoadingFolders(false);
|
||
setFolders(folderData);
|
||
|
||
// Update stats
|
||
setStats({
|
||
total: folderData.length,
|
||
shared: folderData.filter((folder) => folder.access === "shared").length,
|
||
private: folderData.filter((folder) => folder.access === "private").length,
|
||
thisWeek: folderData.filter((folder) => {
|
||
const weekAgo = new Date()
|
||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||
return folder.modified > weekAgo
|
||
}).length,
|
||
permanent: folderData.filter((folder) => folder.storageType === "permanent").length,
|
||
temporary: folderData.filter((folder) => folder.storageType === "temporary").length,
|
||
});
|
||
} else if (!hasFoldersToLoad) {
|
||
setLoadingFolders(false);
|
||
setFolders([]);
|
||
setStats({
|
||
total: 0,
|
||
shared: 0,
|
||
private: 0,
|
||
thisWeek: 0,
|
||
permanent: 0,
|
||
temporary: 0,
|
||
});
|
||
}
|
||
}, [folderProcesses, myFolderProcesses, folderPrivateData, iframeUrl]);
|
||
|
||
useEffect(() => {
|
||
// Load folders from 4NK when folder processes are available
|
||
if (folderProcesses && myFolderProcesses.length >= 0) {
|
||
loadFoldersFrom4NK();
|
||
} else {
|
||
// Fallback: load empty folders if not connected to 4NK
|
||
const mockFolders: FolderData[] = []
|
||
setFolders(mockFolders)
|
||
setStats({
|
||
total: mockFolders.length,
|
||
shared: mockFolders.filter((folder) => folder.access === "shared").length,
|
||
private: mockFolders.filter((folder) => folder.access === "private").length,
|
||
thisWeek: mockFolders.filter((folder) => {
|
||
const weekAgo = new Date()
|
||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||
return folder.modified > weekAgo
|
||
}).length,
|
||
permanent: mockFolders.filter((folder) => folder.storageType === "permanent").length,
|
||
temporary: mockFolders.filter((folder) => folder.storageType === "temporary").length,
|
||
})
|
||
}
|
||
}, [folderProcesses, myFolderProcesses, loadFoldersFrom4NK])
|
||
|
||
// Update folders when private data changes
|
||
useEffect(() => {
|
||
if (folderProcesses && Object.keys(folderPrivateData).length > 0) {
|
||
loadFoldersFrom4NK();
|
||
}
|
||
}, [folderPrivateData, folderProcesses, loadFoldersFrom4NK])
|
||
|
||
// 4NK Authentication handlers
|
||
const handleLogin = useCallback(() => {
|
||
setShowAuthModal(true);
|
||
|
||
}, []);
|
||
|
||
const handleLogout = useCallback(() => {
|
||
UserStore.getInstance().disconnect();
|
||
setIsConnected(false);
|
||
setProcesses(null);
|
||
setMyProcesses([]);
|
||
setUserPairingId(null);
|
||
|
||
// Clear folder-related states
|
||
setFolderProcesses(null);
|
||
setMyFolderProcesses([]);
|
||
setFolderPrivateData({});
|
||
setFolders([]);
|
||
setLoadingFolders(false);
|
||
|
||
// Émettre un événement pour vider les messages locaux
|
||
EventBus.getInstance().emit('CLEAR_CONSOLE');
|
||
|
||
showNotification("info", "Déconnexion réussie");
|
||
}, []);
|
||
|
||
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);
|
||
}, []);
|
||
|
||
|
||
// Fonction pour envoyer une notification dans le chat du dossier
|
||
const sendFolderChatNotification = (folderId: string, message: string, actionType: string) => {
|
||
const folderUsers = users.filter((user) => user.folderRoles[folderId])
|
||
|
||
console.log("Notification envoyée dans le chat du dossier:", {
|
||
folderId,
|
||
recipients: folderUsers.map((u) => ({ name: u.name, role: u.folderRoles[folderId]?.role })),
|
||
message,
|
||
actionType,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
folderUsers.forEach((user) => {
|
||
console.log(`📱 Notification push envoyée à ${user.name} (${user.email})`)
|
||
})
|
||
}
|
||
|
||
// Fonction pour organiser les utilisateurs par rôles
|
||
const organizeUsersForInvitation = (currentFolderId: string) => {
|
||
const organized = {
|
||
folderRoles: {} as { [role: string]: UserWithRoles[] },
|
||
spaceRoles: {} as { [role: string]: UserWithRoles[] },
|
||
otherSpaces: {} as { [spaceName: string]: UserWithRoles[] },
|
||
}
|
||
|
||
users.forEach((user) => {
|
||
if (user.folderRoles[currentFolderId]) {
|
||
const role = user.folderRoles[currentFolderId].role
|
||
if (!organized.folderRoles[role]) organized.folderRoles[role] = []
|
||
organized.folderRoles[role].push(user)
|
||
}
|
||
|
||
const spaceRole = user.spaceRole
|
||
if (!organized.spaceRoles[spaceRole]) organized.spaceRoles[spaceRole] = []
|
||
organized.spaceRoles[spaceRole].push(user)
|
||
|
||
Object.values(user.spaceRoles).forEach((spaceInfo) => {
|
||
if (spaceInfo.spaceName !== "Espace Principal") {
|
||
if (!organized.otherSpaces[spaceInfo.spaceName]) organized.otherSpaces[spaceInfo.spaceName] = []
|
||
organized.otherSpaces[spaceInfo.spaceName].push(user)
|
||
}
|
||
})
|
||
})
|
||
|
||
return organized
|
||
}
|
||
|
||
// Folder actions
|
||
const handleOpenFolder = (folder: FolderData) => {
|
||
// Rediriger vers la page documents avec le filtre du dossier
|
||
router.push(`/dashboard/documents?folder=${encodeURIComponent(folder.name)}`)
|
||
}
|
||
|
||
const handleInviteFolder = (folder: FolderData) => {
|
||
setInviteMessage("")
|
||
setSelectedUser("")
|
||
setSelectedRole("")
|
||
setInviteScope("user")
|
||
setActionModal({ type: "invite", folder, folders: [] })
|
||
}
|
||
|
||
const handleArchiveFolder = (folder: FolderData) => {
|
||
setArchiveReason("")
|
||
setRetentionPeriod("5")
|
||
setActionModal({ type: "archive", folder, folders: [] })
|
||
}
|
||
|
||
const handleStorageConfig = (folder: FolderData) => {
|
||
setStorageDuration(folder.temporaryStorageConfig?.duration.toString() || "30")
|
||
setDataUsage(folder.temporaryStorageConfig?.dataUsage || "")
|
||
setThirdPartyAccess(folder.temporaryStorageConfig?.thirdPartyAccess || "")
|
||
setActionModal({ type: "storage_config", folder, folders: [] })
|
||
}
|
||
|
||
const handleAIAnalysis = (folder: FolderData) => {
|
||
showNotification("info", `Analyse IA en cours pour ${folder.name}...`)
|
||
|
||
// Simuler une analyse IA
|
||
setTimeout(
|
||
() => {
|
||
const analysisResults = [
|
||
`📊 **Analyse du dossier "${folder.name}"**\n\n` +
|
||
`**Contenu :** ${folder.documentsCount} documents analysés (${folder.size})\n` +
|
||
`**Thématiques principales :** ${folder.tags.join(", ")}\n` +
|
||
`**Niveau d'activité :** ${folder.activity.length > 2 ? "Élevé" : "Modéré"} (dernière modification ${formatDate(folder.modified)})\n\n` +
|
||
`**Recommandations :**\n` +
|
||
`• ${folder.storageType === "temporary" ? "Considérer l'archivage vers le stockage permanent" : "Dossier déjà archivé de manière optimale"}\n` +
|
||
`• ${folder.access === "private" ? "Évaluer les possibilités de partage avec l'équipe" : "Partage actuel avec " + folder.members.length + " membre(s)"}\n` +
|
||
`• Dernière activité significative détectée il y a ${Math.floor(Math.random() * 7) + 1} jour(s)\n\n` +
|
||
`**Score de pertinence :** ${Math.floor(Math.random() * 30) + 70}/100`,
|
||
|
||
`🔍 **Analyse approfondie du dossier "${folder.name}"**\n\n` +
|
||
`**Structure documentaire :**\n` +
|
||
`• ${Math.floor(folder.documentsCount * 0.4)} documents principaux\n` +
|
||
`• ${Math.floor(folder.documentsCount * 0.3)} documents de support\n` +
|
||
`• ${Math.floor(folder.documentsCount * 0.3)} documents annexes\n\n` +
|
||
`**Analyse temporelle :**\n` +
|
||
`• Création : ${folder.created.toLocaleDateString("fr-FR")}\n` +
|
||
`• Pic d'activité détecté en ${new Date().toLocaleDateString("fr-FR", { month: "long", year: "numeric" })}\n` +
|
||
`• Tendance : ${Math.random() > 0.5 ? "Croissante" : "Stable"}\n\n` +
|
||
`**Recommandations stratégiques :**\n` +
|
||
`• ${folder.documentsCount > 50 ? "Envisager une réorganisation en sous-dossiers" : "Structure actuelle optimale"}\n` +
|
||
`• ${folder.members.length < 3 ? "Potentiel de collaboration à explorer" : "Équipe collaborative active"}\n` +
|
||
`• Prochaine révision recommandée : ${new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString("fr-FR")}`,
|
||
|
||
`🎯 **Insights IA pour "${folder.name}"**\n\n` +
|
||
`**Analyse sémantique :**\n` +
|
||
`• Cohérence thématique : ${Math.floor(Math.random() * 20) + 80}%\n` +
|
||
`• Mots-clés dominants : ${folder.tags.slice(0, 3).join(", ")}\n` +
|
||
`• Complexité moyenne : ${["Faible", "Modérée", "Élevée"][Math.floor(Math.random() * 3)]}\n\n` +
|
||
`**Patterns détectés :**\n` +
|
||
`• ${Math.random() > 0.5 ? "Cycle de révision régulier identifié" : "Activité sporadique détectée"}\n` +
|
||
`• ${Math.random() > 0.5 ? "Collaboration inter-équipes active" : "Usage principalement individuel"}\n` +
|
||
`• ${folder.storageType === "permanent" ? "Archivage conforme aux bonnes pratiques" : "Optimisation de stockage possible"}\n\n` +
|
||
`**Actions suggérées :**\n` +
|
||
`• ${Math.random() > 0.5 ? "Créer un template basé sur ce dossier" : "Standardiser la nomenclature"}\n` +
|
||
`• ${Math.random() > 0.5 ? "Planifier une session de nettoyage" : "Maintenir la structure actuelle"}\n` +
|
||
`• Prochaine analyse automatique : ${new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toLocaleDateString("fr-FR")}`,
|
||
]
|
||
|
||
const randomAnalysis = analysisResults[Math.floor(Math.random() * analysisResults.length)]
|
||
|
||
// Envoyer l'analyse dans le chat du dossier
|
||
sendFolderChatNotification(folder.id.toString(), `🤖 ${randomAnalysis}`, "ai_analysis")
|
||
|
||
showNotification("success", `Analyse IA terminée pour ${folder.name}. Redirection vers le chat...`)
|
||
|
||
// Rediriger vers le chat après 1.5 secondes
|
||
setTimeout(() => {
|
||
router.push("/dashboard/chat")
|
||
}, 1500)
|
||
},
|
||
2000 + Math.random() * 3000,
|
||
)
|
||
}
|
||
|
||
const handleViewCertificate = (folder: FolderData) => {
|
||
setActionModal({ type: "certificate", folder, folders: [] })
|
||
}
|
||
|
||
const handleViewDocumentsCertificates = (folder: FolderData) => {
|
||
setActionModal({ type: "documents_certificates", folder, folders: [] })
|
||
}
|
||
|
||
const handleDownloadCertificate = (folder: FolderData) => {
|
||
if (folder.status === "validated") {
|
||
showNotification("info", `Téléchargement du certificat blockchain pour le dossier ${folder.name}...`)
|
||
|
||
sendFolderChatNotification(
|
||
folder.id.toString(),
|
||
`🔗 Certificat blockchain du dossier téléchargé`,
|
||
"folder_blockchain_certificate_download",
|
||
)
|
||
|
||
setTimeout(() => {
|
||
showNotification("success", `Certificat blockchain du dossier ${folder.name} téléchargé avec succès`)
|
||
}, 2000)
|
||
}
|
||
}
|
||
|
||
const handleManageRoles = (folder: FolderData) => {
|
||
// Rediriger vers la gestion des rôles du dossier
|
||
router.push(`/dashboard/folders/${folder.id}/roles`)
|
||
}
|
||
|
||
const handleRequestDocument = (folder: FolderData) => {
|
||
setSelectedDocument("")
|
||
setRequestMessage("")
|
||
setActionModal({ type: "request_document", folder, folders: [] })
|
||
}
|
||
|
||
const handleDeleteFolder = (folder: FolderData) => {
|
||
setActionModal({ type: "delete", folder, folders: [] })
|
||
}
|
||
|
||
const handleOpenModal = (type: FolderType) => {
|
||
setFolderType(type);
|
||
setIsModalOpen(true);
|
||
setMenuOpen(false);
|
||
};
|
||
|
||
const handleCloseModal = () => {
|
||
setIsModalOpen(false);
|
||
setFolderType(null);
|
||
};
|
||
|
||
const handleSaveNewFolder = useCallback(
|
||
(folderData: SDKFolderData) => {
|
||
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;
|
||
}
|
||
|
||
// Ajout du type dans les données du dossier
|
||
const folderToCreate = {
|
||
...folderData,
|
||
type: folderType
|
||
};
|
||
|
||
const roles = setDefaultFolderRoles(userPairingId, [], []);
|
||
const folderPrivateFields = FolderPrivateFields;
|
||
|
||
MessageBus.getInstance(iframeUrl)
|
||
.createFolder(folderToCreate, folderPrivateFields, roles)
|
||
.then((_folderCreated: FolderCreated) => {
|
||
const firstStateId = _folderCreated.process.states[0].state_id;
|
||
MessageBus.getInstance(iframeUrl)
|
||
.notifyProcessUpdate(_folderCreated.processId, firstStateId)
|
||
.then(() =>
|
||
MessageBus.getInstance(iframeUrl)
|
||
.validateState(_folderCreated.processId, firstStateId)
|
||
.then(() =>
|
||
MessageBus.getInstance(iframeUrl)
|
||
.getProcesses()
|
||
.then(async (processes: any) => {
|
||
setProcesses(processes)
|
||
setFolderProcesses(processes) // Update folder processes as well
|
||
})
|
||
)
|
||
);
|
||
|
||
setShowCreateFolderModal(false);
|
||
showNotification("success", `Dossier "${folderData.name}" créé avec succès sur 4NK`);
|
||
})
|
||
.catch((error) => {
|
||
console.error('Erreur lors de la création du dossier 4NK:', error);
|
||
showNotification("error", "Erreur lors de la création du dossier");
|
||
});
|
||
},
|
||
[userPairingId, isConnected, iframeUrl, folderType]
|
||
);
|
||
|
||
const handleToggleFavorite = (folderId: number) => {
|
||
const folder = folders.find((f) => f.id === folderId)
|
||
if (!folder) return
|
||
|
||
setFolders((prev) => prev.map((f) => (f.id === folderId ? { ...f, favorite: !f.favorite } : f)))
|
||
|
||
const action = folder.favorite ? "retiré des" : "ajouté aux"
|
||
showNotification("success", `${folder.name} ${action} favoris`)
|
||
|
||
sendFolderChatNotification(folderId.toString(), `⭐ Le dossier a été ${action} favoris`, "favorite")
|
||
}
|
||
|
||
// Bulk actions
|
||
const handleBulkDownload = () => {
|
||
const selectedFolderData = folders.filter((folder) => selectedFolders.includes(folder.id))
|
||
showNotification("info", `Téléchargement de ${selectedFolderData.length} dossier(s)...`)
|
||
|
||
selectedFolderData.forEach((folder) => {
|
||
sendFolderChatNotification(folder.id.toString(), `📥 Le dossier a été téléchargé`, "download")
|
||
})
|
||
|
||
setTimeout(() => {
|
||
showNotification("success", `${selectedFolderData.length} dossier(s) téléchargé(s) avec succès`)
|
||
setSelectedFolders([])
|
||
}, 2000)
|
||
}
|
||
|
||
const handleBulkInvite = () => {
|
||
const selectedFolderData = folders.filter(
|
||
(folder) => selectedFolders.includes(folder.id) && folder.permissions.canInvite,
|
||
)
|
||
if (selectedFolderData.length === 0) {
|
||
showNotification("error", "Aucun dossier sélectionné ne peut être partagé")
|
||
return
|
||
}
|
||
setInviteMessage("")
|
||
setSelectedUser("")
|
||
setSelectedRole("")
|
||
setInviteScope("user")
|
||
setActionModal({ type: "invite", folder: null, folders: selectedFolderData })
|
||
}
|
||
|
||
const handleBulkArchive = () => {
|
||
const selectedFolderData = folders.filter(
|
||
(folder) => selectedFolders.includes(folder.id) && folder.permissions.canArchive,
|
||
)
|
||
if (selectedFolderData.length === 0) {
|
||
showNotification("error", "Aucun dossier sélectionné ne peut être archivé")
|
||
return
|
||
}
|
||
setArchiveReason("")
|
||
setRetentionPeriod("5")
|
||
setActionModal({ type: "archive", folder: null, folders: selectedFolderData })
|
||
}
|
||
|
||
const handleBulkAIAnalysis = () => {
|
||
const selectedFolderData = folders.filter(
|
||
(folder) => selectedFolders.includes(folder.id) && folder.permissions.canAnalyze,
|
||
)
|
||
if (selectedFolderData.length === 0) {
|
||
showNotification("error", "Aucun dossier sélectionné ne peut être analysé")
|
||
return
|
||
}
|
||
|
||
showNotification("info", `Analyse IA en cours pour ${selectedFolderData.length} dossier(s)...`)
|
||
|
||
// Analyser chaque dossier avec un délai échelonné
|
||
selectedFolderData.forEach((folder, index) => {
|
||
setTimeout(() => {
|
||
const bulkAnalysis =
|
||
`📊 **Analyse IA groupée - Dossier "${folder.name}"**\n\n` +
|
||
`**Position dans l'analyse :** ${index + 1}/${selectedFolderData.length}\n` +
|
||
`**Contenu :** ${folder.documentsCount} documents (${folder.size})\n` +
|
||
`**Tags :** ${folder.tags.join(", ")}\n\n` +
|
||
`**Analyse comparative :**\n` +
|
||
`• Taille relative : ${folder.documentsCount > 40 ? "Au-dessus de la moyenne" : "Dans la moyenne"}\n` +
|
||
`• Activité : ${folder.activity.length > 1 ? "Active" : "Modérée"}\n` +
|
||
`• Collaboration : ${folder.members.length} membre(s)\n\n` +
|
||
`**Recommandation :** ${folder.storageType === "temporary" ? "Candidat à l'archivage" : "Archivage optimal"}\n` +
|
||
`**Score global :** ${Math.floor(Math.random() * 30) + 70}/100`
|
||
|
||
sendFolderChatNotification(folder.id.toString(), `🤖 ${bulkAnalysis}`, "bulk_ai_analysis")
|
||
}, index * 1500) // Échelonner les analyses
|
||
})
|
||
|
||
setTimeout(
|
||
() => {
|
||
const totalDocs = selectedFolderData.reduce((sum, folder) => sum + folder.documentsCount, 0)
|
||
showNotification(
|
||
"success",
|
||
`Analyse IA terminée pour ${selectedFolderData.length} dossier(s) (${totalDocs} documents). Redirection vers le chat...`,
|
||
)
|
||
setSelectedFolders([])
|
||
|
||
// Rediriger vers le chat après l'analyse groupée
|
||
setTimeout(() => {
|
||
router.push("/dashboard/chat")
|
||
}, 1500)
|
||
},
|
||
selectedFolderData.length * 1500 + 1000,
|
||
)
|
||
}
|
||
|
||
const handleBulkDelete = () => {
|
||
const selectedFolderData = folders.filter(
|
||
(folder) => selectedFolders.includes(folder.id) && folder.permissions.canDelete,
|
||
)
|
||
if (selectedFolderData.length === 0) {
|
||
showNotification("error", "Aucun dossier sélectionné ne peut être supprimé")
|
||
return
|
||
}
|
||
setActionModal({ type: "delete", folder: null, folders: selectedFolderData })
|
||
}
|
||
|
||
// Modal actions
|
||
const confirmInvite = () => {
|
||
const recipient =
|
||
inviteScope === "user"
|
||
? users.find((u) => u.id === selectedUser)?.name
|
||
: roles.find((r) => r.id === selectedRole)?.name
|
||
|
||
if (actionModal.folder) {
|
||
showNotification("success", `${actionModal.folder.name} partagé avec ${recipient}. Un message a été envoyé.`)
|
||
sendFolderChatNotification(
|
||
actionModal.folder.id.toString(),
|
||
`👥 Le dossier a été partagé avec ${recipient}. Message: ${inviteMessage}`,
|
||
"invite",
|
||
)
|
||
} else if (actionModal.folders.length > 0) {
|
||
actionModal.folders.forEach((folder) => {
|
||
sendFolderChatNotification(
|
||
folder.id.toString(),
|
||
`👥 Le dossier a été partagé avec ${recipient}. Message: ${inviteMessage}`,
|
||
"bulk_invite",
|
||
)
|
||
})
|
||
showNotification(
|
||
"success",
|
||
`${actionModal.folders.length} dossier(s) partagé(s) avec ${recipient}. Messages envoyés.`,
|
||
)
|
||
setSelectedFolders([])
|
||
}
|
||
setActionModal({ type: null, folder: null, folders: [] })
|
||
}
|
||
|
||
const confirmRequestDocument = () => {
|
||
if (actionModal.folder && selectedDocument) {
|
||
const document = actionModal.folder.expectedDocuments.find((doc) => doc.name === selectedDocument)
|
||
if (document) {
|
||
// Trouver l'utilisateur avec le rôle assigné
|
||
const assignedUser = users.find(
|
||
(user) => user.folderRoles[actionModal.folder!.id.toString()]?.role === document.assignedRole,
|
||
)
|
||
|
||
if (assignedUser) {
|
||
// Préparer les données pour le chat
|
||
const messageData = {
|
||
userName: assignedUser.name,
|
||
subject: `Demande de document - ${selectedDocument}`,
|
||
content: `Bonjour ${assignedUser.name},\n\nPouvez-vous fournir le document "${selectedDocument}" pour le dossier "${actionModal.folder.name}" ?\n\n${requestMessage}\n\nMerci !`,
|
||
}
|
||
|
||
// Stocker dans sessionStorage pour le chat
|
||
sessionStorage.setItem("newMessage", JSON.stringify(messageData))
|
||
|
||
showNotification("success", `Demande envoyée à ${assignedUser.name}. Redirection vers le chat...`)
|
||
|
||
// Rediriger vers le chat avec l'utilisateur
|
||
setTimeout(() => {
|
||
router.push(`/dashboard/chat?user=${assignedUser.id}&message=new`)
|
||
}, 1500)
|
||
} else {
|
||
showNotification("error", "Aucun utilisateur trouvé avec le rôle requis pour ce document")
|
||
}
|
||
}
|
||
}
|
||
setActionModal({ type: null, folder: null, folders: [] })
|
||
}
|
||
|
||
const confirmStorageConfig = () => {
|
||
if (actionModal.folder) {
|
||
const updatedFolder = {
|
||
...actionModal.folder,
|
||
temporaryStorageConfig: {
|
||
duration: Number.parseInt(storageDuration),
|
||
dataUsage: dataUsage,
|
||
thirdPartyAccess: thirdPartyAccess,
|
||
},
|
||
modified: new Date(),
|
||
}
|
||
setFolders((prev) => prev.map((f) => (f.id === updatedFolder.id ? updatedFolder : f)))
|
||
showNotification("success", `Configuration du stockage temporaire mise à jour pour ${actionModal.folder.name}`)
|
||
|
||
// Notification dans le chat du dossier
|
||
const message = `⚙️ Configuration du stockage temporaire mise à jour :\n• Durée : ${storageDuration} jours\n• Usage : ${dataUsage}\n• Accès tiers : ${thirdPartyAccess}`
|
||
sendFolderChatNotification(actionModal.folder.id.toString(), message, "storage_config")
|
||
}
|
||
setActionModal({ type: null, folder: null, folders: [] })
|
||
}
|
||
|
||
const confirmArchive = () => {
|
||
if (actionModal.folder) {
|
||
const updatedFolder = {
|
||
...actionModal.folder,
|
||
storageType: "permanent" as const,
|
||
modified: new Date(),
|
||
}
|
||
setFolders((prev) => prev.map((f) => (f.id === updatedFolder.id ? updatedFolder : f)))
|
||
showNotification(
|
||
"success",
|
||
`${actionModal.folder.name} et tous ses documents archivés vers le stockage permanent`,
|
||
)
|
||
|
||
// Notification dans le chat du dossier
|
||
let message = `📦 Le dossier et tous ses ${actionModal.folder.documentsCount} document(s) ont été archivés vers le stockage permanent (conservation: ${retentionPeriod} ans)`
|
||
if (archiveReason.trim()) {
|
||
message += ` - Raison: ${archiveReason}`
|
||
}
|
||
|
||
sendFolderChatNotification(actionModal.folder.id.toString(), message, "archive")
|
||
} else if (actionModal.folders.length > 0) {
|
||
const folderIds = actionModal.folders.map((f) => f.id)
|
||
setFolders((prev) =>
|
||
prev.map((f) =>
|
||
folderIds.includes(f.id)
|
||
? {
|
||
...f,
|
||
storageType: "permanent" as const,
|
||
modified: new Date(),
|
||
}
|
||
: f,
|
||
),
|
||
)
|
||
|
||
actionModal.folders.forEach((folder) => {
|
||
let message = `📦 Le dossier et tous ses ${folder.documentsCount} document(s) ont été archivés vers le stockage permanent (conservation: ${retentionPeriod} ans)`
|
||
if (archiveReason.trim()) {
|
||
message += ` - Raison: ${archiveReason}`
|
||
}
|
||
sendFolderChatNotification(folder.id.toString(), message, "bulk_archive")
|
||
})
|
||
|
||
const totalDocuments = actionModal.folders.reduce((sum, folder) => sum + folder.documentsCount, 0)
|
||
showNotification(
|
||
"success",
|
||
`${actionModal.folders.length} dossier(s) et ${totalDocuments} document(s) archivés vers le stockage permanent`,
|
||
)
|
||
setSelectedFolders([])
|
||
}
|
||
setActionModal({ type: null, folder: null, folders: [] })
|
||
}
|
||
|
||
const confirmCreate = () => {
|
||
const newFolder: FolderData = {
|
||
id: Math.max(...folders.map((f) => f.id)) + 1,
|
||
name: folderName,
|
||
description: folderDescription,
|
||
documentsCount: 0,
|
||
subfoldersCount: 0,
|
||
size: "0 MB",
|
||
created: new Date(),
|
||
modified: new Date(),
|
||
owner: "Utilisateur actuel",
|
||
access: folderAccess,
|
||
members: ["Utilisateur actuel"],
|
||
tags: folderTags
|
||
.split(",")
|
||
.map((tag) => tag.trim())
|
||
.filter((tag) => tag),
|
||
color: folderColor,
|
||
favorite: false,
|
||
storageType: "temporary",
|
||
status: "active",
|
||
type: "general",
|
||
expectedDocuments: [],
|
||
activity: [],
|
||
permissions: {
|
||
canView: true,
|
||
canEdit: true,
|
||
canDelete: true,
|
||
canInvite: true,
|
||
canArchive: true,
|
||
canAnalyze: true,
|
||
},
|
||
documents: [],
|
||
}
|
||
setFolders((prev) => [...prev, newFolder])
|
||
showNotification("success", `Dossier "${folderName}" créé avec succès`)
|
||
setActionModal({ type: null, folder: null, folders: [] })
|
||
}
|
||
|
||
const confirmDelete = () => {
|
||
if (actionModal.folder) {
|
||
sendFolderChatNotification(actionModal.folder.id.toString(), `🗑️ Le dossier a été supprimé`, "delete")
|
||
setFolders((prev) => prev.filter((f) => f.id !== actionModal.folder!.id))
|
||
showNotification("success", `${actionModal.folder.name} supprimé`)
|
||
} else if (actionModal.folders.length > 0) {
|
||
actionModal.folders.forEach((folder) => {
|
||
sendFolderChatNotification(folder.id.toString(), `🗑️ Le dossier a été supprimé`, "bulk_delete")
|
||
})
|
||
const folderIds = actionModal.folders.map((f) => f.id)
|
||
setFolders((prev) => prev.filter((f) => !folderIds.includes(f.id)))
|
||
showNotification("success", `${actionModal.folders.length} dossier(s) supprimé(s)`)
|
||
setSelectedFolders([])
|
||
}
|
||
setActionModal({ type: null, folder: null, folders: [] })
|
||
}
|
||
|
||
const typeFilter = searchParams.get("type")
|
||
|
||
const filteredFolders = folders
|
||
.filter((folder) => {
|
||
if (searchTerm && !folder.name.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||
return false
|
||
}
|
||
if (typeFilter && folder.type !== typeFilter) {
|
||
return false
|
||
}
|
||
if (filterAccess !== "all" && folder.access !== filterAccess) {
|
||
return false
|
||
}
|
||
if (filterOwner !== "all" && folder.owner !== filterOwner) {
|
||
return false
|
||
}
|
||
if (filterStorage !== "all" && folder.storageType !== filterStorage) {
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
.sort((a, b) => {
|
||
let aValue, bValue
|
||
switch (sortBy) {
|
||
case "name":
|
||
aValue = a.name.toLowerCase()
|
||
bValue = b.name.toLowerCase()
|
||
break
|
||
case "size":
|
||
aValue = Number.parseFloat(a.size.replace(/[^\d.]/g, ""))
|
||
bValue = Number.parseFloat(b.size.replace(/[^\d.]/g, ""))
|
||
break
|
||
case "owner":
|
||
aValue = a.owner.toLowerCase()
|
||
bValue = b.owner.toLowerCase()
|
||
break
|
||
case "documents":
|
||
aValue = a.documentsCount
|
||
bValue = b.documentsCount
|
||
break
|
||
case "modified":
|
||
default:
|
||
aValue = a.modified.getTime()
|
||
bValue = b.modified.getTime()
|
||
break
|
||
}
|
||
|
||
if (sortOrder === "asc") {
|
||
return aValue > bValue ? 1 : -1
|
||
} else {
|
||
return aValue < bValue ? 1 : -1
|
||
}
|
||
})
|
||
|
||
|
||
const getFolderColor = (color: string) => {
|
||
const colorObj = colors.find((c) => c.id === color)
|
||
return colorObj?.class || "text-gray-600 bg-gray-100"
|
||
}
|
||
|
||
const isNewFolder = (folder: FolderData) => {
|
||
const now = Date.now()
|
||
const diffMs = now - folder.modified.getTime()
|
||
const twoDaysMs = 2 * 24 * 60 * 60 * 1000
|
||
return diffMs <= twoDaysMs
|
||
}
|
||
|
||
const getStorageIcon = (storageType: string) => {
|
||
return storageType === "permanent" ? (
|
||
<Cloud className="h-4 w-4 text-blue-600" />
|
||
) : (
|
||
<HardDrive className="h-4 w-4 text-gray-600" />
|
||
)
|
||
}
|
||
|
||
const getRoleIcon = (role: string) => {
|
||
switch (role) {
|
||
case "owner":
|
||
return <Crown className="h-4 w-4 text-yellow-600" />
|
||
case "editor":
|
||
return <FileText className="h-4 w-4 text-blue-600" />
|
||
case "validator":
|
||
return <Shield className="h-4 w-4 text-green-600" />
|
||
case "contributor":
|
||
return <UserPlus className="h-4 w-4 text-purple-600" />
|
||
case "viewer":
|
||
return <FileText className="h-4 w-4 text-gray-600" />
|
||
case "admin":
|
||
return <Shield className="h-4 w-4 text-red-600" />
|
||
case "manager":
|
||
return <Users className="h-4 w-4 text-orange-600" />
|
||
case "user":
|
||
return <User className="h-4 w-4 text-blue-600" />
|
||
case "guest":
|
||
return <User className="h-4 w-4 text-gray-400" />
|
||
default:
|
||
return <User className="h-4 w-4 text-gray-600" />
|
||
}
|
||
}
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
switch (status) {
|
||
case "active":
|
||
return <Badge className="bg-green-100 text-green-800 border-green-200">Actif</Badge>
|
||
case "pending":
|
||
return <Badge className="bg-orange-100 text-orange-800 border-orange-200">En attente</Badge>
|
||
case "completed":
|
||
return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Terminé</Badge>
|
||
case "archived":
|
||
return <Badge className="bg-gray-100 text-gray-800 border-gray-200">Archivé</Badge>
|
||
case "validated":
|
||
return <Badge className="bg-green-300 text-green-800 border-green-400">Validé</Badge>
|
||
default:
|
||
return <Badge className="bg-gray-100 text-gray-800 border-gray-200">Inconnu</Badge>
|
||
}
|
||
}
|
||
|
||
const formatDate = (date: Date) => {
|
||
const now = new Date()
|
||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60)
|
||
|
||
if (diffInHours < 1) {
|
||
return "Il y a quelques minutes"
|
||
} else if (diffInHours < 24) {
|
||
return `Il y a ${Math.floor(diffInHours)} heure${Math.floor(diffInHours) > 1 ? "s" : ""}`
|
||
} else if (diffInHours < 48) {
|
||
return "Hier"
|
||
} else {
|
||
return date.toLocaleDateString("fr-FR")
|
||
}
|
||
}
|
||
|
||
const toggleFolderSelection = (folderId: number) => {
|
||
setSelectedFolders((prev) => (prev.includes(folderId) ? prev.filter((id) => id !== folderId) : [...prev, folderId]))
|
||
}
|
||
|
||
const selectAllFolders = () => {
|
||
if (selectedFolders.length === filteredFolders.length) {
|
||
setSelectedFolders([])
|
||
} else {
|
||
setSelectedFolders(filteredFolders.map((folder) => folder.id))
|
||
}
|
||
}
|
||
|
||
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>
|
||
)}
|
||
|
||
|
||
{/* Modal */}
|
||
{isModalOpen && (
|
||
<FolderModal
|
||
isOpen={isModalOpen}
|
||
onClose={handleCloseModal}
|
||
onSave={handleSaveNewFolder}
|
||
onCancel={handleCloseModal}
|
||
folderType={folderType || "general"}
|
||
/>
|
||
)}
|
||
|
||
{/* 4NK Authentication Modal */}
|
||
{showAuthModal && (
|
||
<AuthModal
|
||
isOpen={showAuthModal}
|
||
onConnect={handleAuthConnect}
|
||
onClose={handleAuthClose}
|
||
iframeUrl={iframeUrl}
|
||
/>
|
||
)}
|
||
|
||
{/* 4NK Iframe - only show when connected */}
|
||
{isConnected && <Iframe iframeUrl={iframeUrl} />}
|
||
</div>
|
||
)
|
||
}
|