550 lines
20 KiB
TypeScript
550 lines
20 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect, useCallback } from "react"
|
||
import { Card, CardContent } from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import {
|
||
Folder,
|
||
Search,
|
||
FolderPlus,
|
||
Clock,
|
||
ChevronRight,
|
||
SortAsc,
|
||
SortDesc,
|
||
X,
|
||
MessageSquare,
|
||
List,
|
||
} 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"
|
||
import FolderChat from "@/components/4nk/FolderChat"
|
||
|
||
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);
|
||
const [activeTab, setActiveTab] = useState<"folders" | "chat">("folders");
|
||
|
||
// 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 = useCallback(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);
|
||
}
|
||
}, [myFolderProcesses]);
|
||
|
||
// 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;
|
||
const missingPrivateData: Array<{processId: string, stateId: string}> = [];
|
||
|
||
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, mark as incomplete and collect missing data
|
||
if (!privateData) {
|
||
hasAllPrivateData = false;
|
||
missingPrivateData.push({processId, stateId: latestState.state_id});
|
||
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);
|
||
// Fetch missing private data (but only once per missing item)
|
||
missingPrivateData.forEach(({processId, stateId}) => {
|
||
if (!folderPrivateData[stateId]) {
|
||
fetchFolderPrivateData(processId, stateId);
|
||
}
|
||
});
|
||
} 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]);
|
||
|
||
// 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]
|
||
);
|
||
|
||
// 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">
|
||
{activeTab === "folders" && (
|
||
<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>
|
||
|
||
{/* Tabs */}
|
||
<div className="flex items-center space-x-4 mb-4">
|
||
<div className="flex bg-gray-700 rounded-lg p-1">
|
||
<Button
|
||
variant={activeTab === "folders" ? "default" : "ghost"}
|
||
size="sm"
|
||
onClick={() => setActiveTab("folders")}
|
||
className="flex items-center space-x-2"
|
||
>
|
||
<List className="h-4 w-4" />
|
||
<span>Dossiers</span>
|
||
</Button>
|
||
<Button
|
||
variant={activeTab === "chat" ? "default" : "ghost"}
|
||
size="sm"
|
||
onClick={() => setActiveTab("chat")}
|
||
className="flex items-center space-x-2"
|
||
>
|
||
<MessageSquare className="h-4 w-4" />
|
||
<span>Chat</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search and filters - only show for folders tab */}
|
||
{activeTab === "folders" && (
|
||
<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 bg-gray-900">
|
||
{activeTab === "folders" ? (
|
||
<div className="p-6">
|
||
{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>
|
||
) : (
|
||
<FolderChat
|
||
heightClass="h-full"
|
||
folderProcesses={folderProcesses}
|
||
myFolderProcesses={myFolderProcesses}
|
||
folderPrivateData={folderPrivateData}
|
||
/>
|
||
)}
|
||
</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>
|
||
)
|
||
}
|