2025-10-23 16:44:22 +02:00

507 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

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

"use client"
import { useState, useEffect, useCallback } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Folder,
Search,
FolderPlus,
Clock,
ChevronRight,
SortAsc,
SortDesc,
X,
} from "lucide-react"
import { FolderData, FolderCreated, FolderPrivateFields, setDefaultFolderRoles } from "@/lib/4nk/models/FolderData"
import MessageBus from "@/lib/4nk/MessageBus"
import { iframeUrl } from "@/app/page"
import UserStore from "@/lib/4nk/UserStore"
import FolderModal from "@/components/4nk/FolderModal"
import AuthModal from "@/components/4nk/AuthModal"
import Iframe from "@/components/4nk/Iframe"
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
export default function FoldersPage() {
const [searchTerm, setSearchTerm] = useState("")
const [sortBy, setSortBy] = useState("updated_at")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
const [currentPath, setCurrentPath] = useState<string[]>(["Racine"])
const [folderType, setFolderType] = useState<FolderType | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
// 4NK Integration states
const [isConnected, setIsConnected] = useState(false)
const [showAuthModal, setShowAuthModal] = useState(false)
const [processes, setProcesses] = useState<any>(null)
const [myProcesses, setMyProcesses] = useState<string[]>([])
const [userPairingId, setUserPairingId] = useState<string | null>(null)
const [folderProcesses, setFolderProcesses] = useState<any>(null)
const [myFolderProcesses, setMyFolderProcesses] = useState<string[]>([])
const [folderPrivateData, setFolderPrivateData] = useState<Record<string, Record<string, any>>>({})
const [loadingFolders, setLoadingFolders] = useState(false)
// Modal states
const [notification, setNotification] = useState<{ type: "success" | "error" | "info"; message: string } | null>(null)
const [folders, setFolders] = useState<FolderData[]>([])
const [stats, setStats] = useState({
total: 0
})
// Notification system
const showNotification = (type: "success" | "error" | "info", message: string) => {
setNotification({ type, message })
setTimeout(() => setNotification(null), 5000)
}
// Function to fetch folder private data
const fetchFolderPrivateData = async (processId: string, stateId: string) => {
if (!myFolderProcesses.includes(processId)) return;
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const data = await messageBus.getData(processId, stateId);
setFolderPrivateData(prev => ({ ...prev, [stateId]: data }));
} catch (err) {
console.error('Error fetching folder private data:', err);
}
};
// Function to load folders from 4NK processes (adapted to new FolderData model)
const loadFoldersFrom4NK = useCallback(() => {
if (!folderProcesses) return;
const folderData: FolderData[] = [];
let hasAllPrivateData = true;
let hasFoldersToLoad = false;
Object.entries(folderProcesses).forEach(([processId, process]: [string, any]) => {
// Only include processes that belong to the user (myFolderProcesses)
if (!myFolderProcesses.includes(processId)) return;
// Check if this process has a folderNumber in pcd_commitment
const latestState = process.states[0];
if (!latestState) return;
const folderNumber = latestState.pcd_commitment?.folderNumber;
if (!folderNumber) return; // Skip processes without folderNumber
hasFoldersToLoad = true; // We have at least one folder to load
// Get private data for this state if available
const privateData = folderPrivateData[latestState.state_id];
// If we don't have private data yet, trigger fetch and mark as incomplete
if (!privateData) {
hasAllPrivateData = false;
setTimeout(() => fetchFolderPrivateData(processId, latestState.state_id), 0);
return; // Skip creating folder until we have private data
}
// Create folder with new simplified model
const folder: FolderData = {
folderNumber: folderNumber,
name: privateData.name || `Dossier ${folderNumber}`,
description: privateData.description || '',
created_at: privateData.created_at || new Date().toISOString(),
updated_at: privateData.updated_at || new Date().toISOString(),
notes: privateData.notes || []
};
folderData.push(folder);
});
// Manage loading state
if (hasFoldersToLoad && !hasAllPrivateData) {
setLoadingFolders(true);
} else if (hasAllPrivateData) {
setLoadingFolders(false);
setFolders(folderData);
// Update stats
setStats({
total: folderData.length
});
}
}, [folderProcesses, myFolderProcesses, folderPrivateData, fetchFolderPrivateData]);
// 4NK Integration useEffects
useEffect(() => {
const userStore = UserStore.getInstance();
const connected = userStore.isConnected();
const pairingId = userStore.getUserPairingId();
console.log('Initial 4NK state:', { connected, pairingId });
setIsConnected(connected);
setUserPairingId(pairingId);
}, []);
useEffect(() => {
const handleConnectionFlow = async () => {
if (!isConnected) return;
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const userStore = UserStore.getInstance();
let pairingId = userStore.getUserPairingId();
// 1⃣ Créer ou récupérer le pairing
if (!pairingId) {
pairingId = await messageBus.createUserPairing();
console.log("✅ Pairing created:", pairingId);
if (pairingId) {
userStore.pair(pairingId);
setUserPairingId(pairingId);
}
} else {
console.log("🔗 Already paired with ID:", pairingId);
}
// 2⃣ Charger les processes
const processes = await messageBus.getProcesses();
setProcesses(processes);
setFolderProcesses(processes);
// 3⃣ Charger les myProcesses
const myProcesses = await messageBus.getMyProcesses();
setMyProcesses(myProcesses);
setMyFolderProcesses(myProcesses);
} catch (err) {
console.error("❌ Error during pairing or process loading:", err);
}
};
handleConnectionFlow();
}, [isConnected, iframeUrl]);
// Load folders from 4NK when folder processes are available
useEffect(() => {
if (folderProcesses && myFolderProcesses.length >= 0) {
loadFoldersFrom4NK();
}
}, [folderProcesses, myFolderProcesses, loadFoldersFrom4NK]);
// Update folders when private data changes
useEffect(() => {
if (folderProcesses && Object.keys(folderPrivateData).length > 0) {
loadFoldersFrom4NK();
}
}, [folderPrivateData, loadFoldersFrom4NK, folderProcesses]);
// Filter and sort folders
const filteredFolders = folders.filter(folder => {
const matchesSearch = folder.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
folder.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
folder.folderNumber.toLowerCase().includes(searchTerm.toLowerCase())
return matchesSearch
})
const sortedFolders = [...filteredFolders].sort((a, b) => {
let aValue: any, bValue: any
switch (sortBy) {
case "name":
aValue = a.name.toLowerCase()
bValue = b.name.toLowerCase()
break
case "created_at":
aValue = new Date(a.created_at)
bValue = new Date(b.created_at)
break
case "updated_at":
aValue = new Date(a.updated_at)
bValue = new Date(b.updated_at)
break
default:
aValue = a.name.toLowerCase()
bValue = b.name.toLowerCase()
}
if (sortOrder === "asc") {
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
} else {
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0
}
})
// Modal handlers
const handleOpenModal = (type: FolderType) => {
setFolderType(type);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setFolderType(null);
};
const handleSaveNewFolder = useCallback(
(folderData: FolderData) => {
if (!isConnected || !userPairingId) {
console.error('Conditions non remplies:', { isConnected, userPairingId });
showNotification(
"error",
`Vous devez être connecté à 4NK pour créer un dossier (Connected: ${isConnected}, PairingId: ${userPairingId ? 'OK' : 'NULL'})`
);
return;
}
const roles = setDefaultFolderRoles(userPairingId);
const folderPrivateFields = FolderPrivateFields;
MessageBus.getInstance(iframeUrl)
.createFolder(folderData, folderPrivateFields, roles)
.then((_folderCreated: FolderCreated) => {
const firstStateId = _folderCreated.process.states[0].state_id;
MessageBus.getInstance(iframeUrl)
.notifyProcessUpdate(_folderCreated.processId, firstStateId)
.then(async () => {
// Recharger les processes et myProcesses
const messageBus = MessageBus.getInstance(iframeUrl);
const [processes, myProcesses] = await Promise.all([
messageBus.getProcesses(),
messageBus.getMyProcesses()
]);
setProcesses(processes);
setFolderProcesses(processes);
setMyProcesses(myProcesses);
setMyFolderProcesses(myProcesses);
showNotification("success", "Dossier créé avec succès !");
handleCloseModal();
});
})
.catch((error: any) => {
console.error('Erreur lors de la création du dossier:', error);
showNotification("error", "Erreur lors de la création du dossier");
});
},
[isConnected, userPairingId, loadFoldersFrom4NK]
);
// Auth connection handler
const handleAuthConnect = useCallback(() => {
setIsConnected(true);
setShowAuthModal(false);
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
showNotification("success", "Connexion 4NK réussie");
}, []);
const handleAuthClose = useCallback(() => {
setShowAuthModal(false);
}, []);
return (
<div className="flex h-screen bg-gray-900">
{/* Sidebar */}
<div className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
<div className="p-6 border-b border-gray-700">
<h1 className="text-xl font-semibold text-gray-100">Dossiers</h1>
<p className="text-sm text-gray-400 mt-1">Gérez vos dossiers 4NK</p>
</div>
<div className="flex-1 p-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Total</span>
<span className="font-medium text-gray-100">{stats.total}</span>
</div>
</div>
</div>
{/* 4NK Connection Status */}
<div className="p-4 border-t border-gray-700">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-100">4NK Status</span>
<Badge variant={isConnected ? "default" : "secondary"}>
{isConnected ? "Connecté" : "Déconnecté"}
</Badge>
</div>
{!isConnected && (
<Button
size="sm"
className="w-full"
onClick={() => setShowAuthModal(true)}
>
Se connecter à 4NK
</Button>
)}
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="bg-gray-800 border-b border-gray-700 p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
{currentPath.map((path, index) => (
<div key={index} className="flex items-center">
{index > 0 && <ChevronRight className="h-4 w-4 text-gray-400 mx-1" />}
<span className="text-sm text-gray-300">{path}</span>
</div>
))}
</div>
<div className="flex items-center space-x-2">
<Button
onClick={() => handleOpenModal("autre")}
disabled={!isConnected}
className="flex items-center space-x-2"
>
<FolderPlus className="h-4 w-4" />
<span>Nouveau dossier</span>
</Button>
</div>
</div>
{/* Search and filters */}
<div className="flex items-center space-x-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Rechercher des dossiers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
>
{sortOrder === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 p-6 bg-gray-900">
{loadingFolders ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-400">Chargement des dossiers...</p>
</div>
</div>
) : sortedFolders.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Folder className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-100 mb-2">Aucun dossier</h3>
<p className="text-gray-400 mb-4">
{searchTerm ? "Aucun dossier ne correspond à votre recherche." : "Commencez par créer votre premier dossier."}
</p>
{!searchTerm && (
<Button
onClick={() => handleOpenModal("autre")}
disabled={!isConnected}
>
<FolderPlus className="h-4 w-4 mr-2" />
Créer un dossier
</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>
)
}