Compare commits

..

No commits in common. "c4edfbd6de8d14bbeeee4afda44122771a088ebd" and "8ee7b8a1775c8c286935b58f7096542e5b370932" have entirely different histories.

14 changed files with 706 additions and 1042 deletions

View File

@ -56,8 +56,6 @@ import Iframe from "@/components/4nk/Iframe"
import MessageBus from "@/lib/4nk/MessageBus" import MessageBus from "@/lib/4nk/MessageBus"
import EventBus from "@/lib/4nk/EventBus" import EventBus from "@/lib/4nk/EventBus"
import UserStore from "@/lib/4nk/UserStore" import UserStore from "@/lib/4nk/UserStore"
import ProcessesViewer from "@/components/ProcessesViewer"
import { iframeUrl } from "@/app/page"
interface FolderData { interface FolderData {
id: number id: number
@ -112,16 +110,16 @@ interface FolderData {
interface ActionModal { interface ActionModal {
type: type:
| "invite" | "invite"
| "delete" | "delete"
| "create" | "create"
| "edit" | "edit"
| "archive" | "archive"
| "request_document" | "request_document"
| "storage_config" | "storage_config"
| "certificate" | "certificate"
| "documents_certificates" | "documents_certificates"
| null | null
folder: FolderData | null folder: FolderData | null
folders: FolderData[] folders: FolderData[]
} }
@ -153,8 +151,6 @@ interface Role {
level: "folder" | "space" | "global" level: "folder" | "space" | "global"
} }
type FolderType = "contrat" | "projet" | "rapport" | "finance" | "rh" | "marketing";
export default function FoldersPage() { export default function FoldersPage() {
const router = useRouter() const router = useRouter()
const [viewMode, setViewMode] = useState<'list'>('list') const [viewMode, setViewMode] = useState<'list'>('list')
@ -169,9 +165,6 @@ export default function FoldersPage() {
const [currentPath, setCurrentPath] = useState<string[]>(["Racine"]) const [currentPath, setCurrentPath] = useState<string[]>(["Racine"])
const [actionModal, setActionModal] = useState<ActionModal>({ type: null, folder: null, folders: [] }) const [actionModal, setActionModal] = useState<ActionModal>({ type: null, folder: null, folders: [] })
const [showCreateFolderModal, setShowCreateFolderModal] = useState(false) 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 // 4NK Integration states
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
@ -180,6 +173,7 @@ export default function FoldersPage() {
const [myProcesses, setMyProcesses] = useState<string[]>([]) const [myProcesses, setMyProcesses] = useState<string[]>([])
const [userPairingId, setUserPairingId] = useState<string | null>(null) const [userPairingId, setUserPairingId] = useState<string | null>(null)
const [pairingIdInitialized, setPairingIdInitialized] = useState(false) const [pairingIdInitialized, setPairingIdInitialized] = useState(false)
const iframeUrl = 'https://dev3.4nkweb.com'
// Modal states // Modal states
const [inviteMessage, setInviteMessage] = useState("") const [inviteMessage, setInviteMessage] = useState("")
@ -344,9 +338,9 @@ export default function FoldersPage() {
const userStore = UserStore.getInstance(); const userStore = UserStore.getInstance();
const connected = userStore.isConnected(); const connected = userStore.isConnected();
const pairingId = userStore.getUserPairingId(); const pairingId = userStore.getUserPairingId();
console.log('Initialisation 4NK:', { connected, pairingId }); console.log('Initialisation 4NK:', { connected, pairingId });
setIsConnected(connected); setIsConnected(connected);
setUserPairingId(pairingId); setUserPairingId(pairingId);
setPairingIdInitialized(true); setPairingIdInitialized(true);
@ -361,7 +355,7 @@ export default function FoldersPage() {
}); });
}); });
} }
}, [isConnected, iframeUrl]); }, [isConnected]);
useEffect(() => { useEffect(() => {
if (isConnected && processes !== null) { if (isConnected && processes !== null) {
@ -378,11 +372,11 @@ export default function FoldersPage() {
useEffect(() => { useEffect(() => {
const handleStorageChange = (e: StorageEvent) => { const handleStorageChange = (e: StorageEvent) => {
console.log('Storage change détecté:', e.key, e.newValue ? 'ajouté' : 'supprimé'); console.log('Storage change détecté:', e.key, e.newValue ? 'ajouté' : 'supprimé');
// Si un token d'accès vient d'être ajouté // Si un token d'accès vient d'être ajouté
if (e.key === 'accessToken' && e.newValue) { if (e.key === 'accessToken' && e.newValue) {
console.log('Token d\'accès détecté, récupération du userPairingId...'); console.log('Token d\'accès détecté, récupération du userPairingId...');
// Attendre un peu que les deux tokens soient bien en place // Attendre un peu que les deux tokens soient bien en place
setTimeout(() => { setTimeout(() => {
const userStore = UserStore.getInstance(); const userStore = UserStore.getInstance();
@ -407,7 +401,7 @@ export default function FoldersPage() {
// Écouter les changements de sessionStorage // Écouter les changements de sessionStorage
window.addEventListener('storage', handleStorageChange); window.addEventListener('storage', handleStorageChange);
// Vérification initiale au chargement // Vérification initiale au chargement
const userStore = UserStore.getInstance(); const userStore = UserStore.getInstance();
if (userStore.isConnected()) { if (userStore.isConnected()) {
@ -714,7 +708,7 @@ export default function FoldersPage() {
} }
loadFolders() loadFolders()
}, []); }, [])
// Notification system // Notification system
const showNotification = (type: "success" | "error" | "info", message: string) => { const showNotification = (type: "success" | "error" | "info", message: string) => {
@ -733,15 +727,14 @@ export default function FoldersPage() {
setProcesses(null); setProcesses(null);
setMyProcesses([]); setMyProcesses([]);
setUserPairingId(null); setUserPairingId(null);
// Émettre un événement pour vider les messages locaux // Émettre un événement pour vider les messages locaux
EventBus.getInstance().emit('CLEAR_CONSOLE'); EventBus.getInstance().emit('CLEAR_CONSOLE');
showNotification("info", "Déconnexion réussie"); showNotification("info", "Déconnexion réussie");
}, []); }, []);
const handleAuthConnect = useCallback(() => { const handleAuthConnect = useCallback(() => {
setIsConnected(true);
setShowAuthModal(false); setShowAuthModal(false);
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId'); console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
showNotification("success", "Connexion 4NK réussie"); showNotification("success", "Connexion 4NK réussie");
@ -759,7 +752,7 @@ export default function FoldersPage() {
userStoreConnected: UserStore.getInstance().isConnected(), userStoreConnected: UserStore.getInstance().isConnected(),
userStorePairingId: UserStore.getInstance().getUserPairingId() userStorePairingId: UserStore.getInstance().getUserPairingId()
}); });
// D'abord essayer de synchroniser depuis UserStore // D'abord essayer de synchroniser depuis UserStore
const userStorePairingId = UserStore.getInstance().getUserPairingId(); const userStorePairingId = UserStore.getInstance().getUserPairingId();
if (userStorePairingId) { if (userStorePairingId) {
@ -768,7 +761,7 @@ export default function FoldersPage() {
showNotification("success", `UserPairingId synchronisé: ${userStorePairingId.substring(0, 8)}...`); showNotification("success", `UserPairingId synchronisé: ${userStorePairingId.substring(0, 8)}...`);
return; return;
} }
// Sinon récupérer depuis MessageBus // Sinon récupérer depuis MessageBus
if (isConnected) { if (isConnected) {
const messageBus = MessageBus.getInstance(iframeUrl); const messageBus = MessageBus.getInstance(iframeUrl);
@ -874,42 +867,42 @@ export default function FoldersPage() {
() => { () => {
const analysisResults = [ const analysisResults = [
`📊 **Analyse du dossier "${folder.name}"**\n\n` + `📊 **Analyse du dossier "${folder.name}"**\n\n` +
`**Contenu :** ${folder.documentsCount} documents analysés (${folder.size})\n` + `**Contenu :** ${folder.documentsCount} documents analysés (${folder.size})\n` +
`**Thématiques principales :** ${folder.tags.join(", ")}\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` + `**Niveau d'activité :** ${folder.activity.length > 2 ? "Élevé" : "Modéré"} (dernière modification ${formatDate(folder.modified)})\n\n` +
`**Recommandations :**\n` + `**Recommandations :**\n` +
`${folder.storageType === "temporary" ? "Considérer l'archivage vers le stockage permanent" : "Dossier déjà archivé de manière optimale"}\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` + `${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` + `• 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`, `**Score de pertinence :** ${Math.floor(Math.random() * 30) + 70}/100`,
`🔍 **Analyse approfondie du dossier "${folder.name}"**\n\n` + `🔍 **Analyse approfondie du dossier "${folder.name}"**\n\n` +
`**Structure documentaire :**\n` + `**Structure documentaire :**\n` +
`${Math.floor(folder.documentsCount * 0.4)} documents principaux\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 de support\n` +
`${Math.floor(folder.documentsCount * 0.3)} documents annexes\n\n` + `${Math.floor(folder.documentsCount * 0.3)} documents annexes\n\n` +
`**Analyse temporelle :**\n` + `**Analyse temporelle :**\n` +
`• Création : ${folder.created.toLocaleDateString("fr-FR")}\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` + `• 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` + `• Tendance : ${Math.random() > 0.5 ? "Croissante" : "Stable"}\n\n` +
`**Recommandations stratégiques :**\n` + `**Recommandations stratégiques :**\n` +
`${folder.documentsCount > 50 ? "Envisager une réorganisation en sous-dossiers" : "Structure actuelle optimale"}\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` + `${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")}`, `• Prochaine révision recommandée : ${new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString("fr-FR")}`,
`🎯 **Insights IA pour "${folder.name}"**\n\n` + `🎯 **Insights IA pour "${folder.name}"**\n\n` +
`**Analyse sémantique :**\n` + `**Analyse sémantique :**\n` +
`• Cohérence thématique : ${Math.floor(Math.random() * 20) + 80}%\n` + `• Cohérence thématique : ${Math.floor(Math.random() * 20) + 80}%\n` +
`• Mots-clés dominants : ${folder.tags.slice(0, 3).join(", ")}\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` + `• Complexité moyenne : ${["Faible", "Modérée", "Élevée"][Math.floor(Math.random() * 3)]}\n\n` +
`**Patterns détectés :**\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 ? "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` + `${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` + `${folder.storageType === "permanent" ? "Archivage conforme aux bonnes pratiques" : "Optimisation de stockage possible"}\n\n` +
`**Actions suggérées :**\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 ? "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` + `${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")}`, `• Prochaine analyse automatique : ${new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toLocaleDateString("fr-FR")}`,
] ]
const randomAnalysis = analysisResults[Math.floor(Math.random() * analysisResults.length)] const randomAnalysis = analysisResults[Math.floor(Math.random() * analysisResults.length)]
@ -967,65 +960,52 @@ export default function FoldersPage() {
setActionModal({ type: "delete", folder, folders: [] }) setActionModal({ type: "delete", folder, folders: [] })
} }
const handleOpenModal = (type: FolderType) => { const handleCreateFolder = () => {
setFolderType(type); setShowCreateFolderModal(true)
setIsModalOpen(true); }
setMenuOpen(false);
};
const handleCloseModal = () => { const handleSaveNewFolder = useCallback((folderData: SDKFolderData) => {
setIsModalOpen(false); console.log('Debug - handleSaveNewFolder:', {
setFolderType(null); isConnected,
}; userPairingId,
userPairingIdType: typeof userPairingId,
const handleSaveNewFolder = useCallback( userStoreConnected: UserStore.getInstance().isConnected(),
(folderData: SDKFolderData) => { userStorePairingId: UserStore.getInstance().getUserPairingId()
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
};
if (userPairingId !== null && isConnected) {
const roles = setDefaultFolderRoles(userPairingId, [], []); const roles = setDefaultFolderRoles(userPairingId, [], []);
const folderPrivateFields = FolderPrivateFields; const folderPrivateFields = FolderPrivateFields;
MessageBus.getInstance(iframeUrl) MessageBus.getInstance(iframeUrl).createFolder(folderData, folderPrivateFields, roles).then((_folderCreated: FolderCreated) => {
.createFolder(folderToCreate, folderPrivateFields, roles) MessageBus.getInstance(iframeUrl).notifyProcessUpdate(_folderCreated.processId, _folderCreated.process.states[0].state_id).then(() => {
.then((_folderCreated: FolderCreated) => { MessageBus.getInstance(iframeUrl).validateState(_folderCreated.processId, _folderCreated.process.states[0].state_id).then((_updatedProcess: any) => {
const firstStateId = _folderCreated.process.states[0].state_id; MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => {
MessageBus.getInstance(iframeUrl) setProcesses(processes);
.notifyProcessUpdate(_folderCreated.processId, firstStateId) });
.then(() => });
MessageBus.getInstance(iframeUrl)
.validateState(_folderCreated.processId, firstStateId)
.then(() =>
MessageBus.getInstance(iframeUrl)
.getProcesses()
.then(async (processes: any) => {
setProcesses(processes)
})
)
);
setShowCreateFolderModal(false);
showNotification("success", `Dossier "${folderData.name}" créé avec succès sur 4NK`);
}) })
.catch((error) => { }).catch((error) => {
console.error('Erreur lors de la création du dossier 4NK:', error); console.error('Erreur lors de la création du dossier 4NK:', error);
showNotification("error", "Erreur lors de la création du dossier"); showNotification("error", "Erreur lors de la création du dossier");
}); });
},
[userPairingId, isConnected, iframeUrl, folderType] setShowCreateFolderModal(false);
); showNotification("success", `Dossier "${folderData.name}" créé avec succès sur 4NK`);
} else {
console.error('Conditions non remplies:', {
userPairingIdCheck: userPairingId !== null,
isConnectedCheck: isConnected,
actualUserPairingId: userPairingId,
actualIsConnected: isConnected
});
showNotification("error", `Vous devez être connecté à 4NK pour créer un dossier (Connected: ${isConnected}, PairingId: ${userPairingId ? 'OK' : 'NULL'})`);
}
}, [userPairingId, isConnected, iframeUrl]);
const handleCancelCreateFolder = () => {
setShowCreateFolderModal(false)
}
const handleToggleFavorite = (folderId: number) => { const handleToggleFavorite = (folderId: number) => {
const folder = folders.find((f) => f.id === folderId) const folder = folders.find((f) => f.id === folderId)
@ -1253,10 +1233,10 @@ export default function FoldersPage() {
prev.map((f) => prev.map((f) =>
folderIds.includes(f.id) folderIds.includes(f.id)
? { ? {
...f, ...f,
storageType: "permanent" as const, storageType: "permanent" as const,
modified: new Date(), modified: new Date(),
} }
: f, : f,
), ),
) )
@ -1471,12 +1451,13 @@ export default function FoldersPage() {
{/* Notification */} {/* Notification */}
{notification && ( {notification && (
<div <div
className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg flex items-center space-x-2 ${notification.type === "success" className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg flex items-center space-x-2 ${
? "bg-green-100 text-green-800 border border-green-200" notification.type === "success"
: notification.type === "error" ? "bg-green-100 text-green-800 border border-green-200"
? "bg-red-100 text-red-800 border border-red-200" : notification.type === "error"
: "bg-blue-100 text-blue-800 border border-blue-200" ? "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 === "success" && <CheckCircle className="h-5 w-5" />}
{notification.type === "error" && <XCircle className="h-5 w-5" />} {notification.type === "error" && <XCircle className="h-5 w-5" />}
@ -1493,7 +1474,7 @@ export default function FoldersPage() {
<div> <div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900">Dossiers</h1> <h1 className="text-2xl font-bold text-gray-900">Dossiers</h1>
<Badge <Badge
variant={isConnected ? "default" : "secondary"} variant={isConnected ? "default" : "secondary"}
className={isConnected ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"} className={isConnected ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}
> >
@ -1514,45 +1495,23 @@ export default function FoldersPage() {
<Upload className="h-4 w-4 mr-2" /> <Upload className="h-4 w-4 mr-2" />
Importer Importer
</Button> </Button>
{isConnected ? ( {isConnected ? (
<div className="flex gap-2"> <>
{/* Nouveau dossier avec menu */} <Button size="sm" onClick={handleCreateFolder}>
<div className="relative"> <FolderPlus className="h-4 w-4 mr-2" />
<Button size="sm" onClick={() => setMenuOpen(!menuOpen)}> Nouveau dossier
<FolderPlus className="h-4 w-4 mr-2" /> </Button>
Nouveau dossier
</Button>
{menuOpen && (
<div className="absolute mt-1 right-0 w-48 bg-white border border-gray-200 rounded shadow-lg z-50">
{['contrat', 'projet', 'rapport', 'finance', 'rh', 'marketing'].map((type) => (
<button
key={type}
className="w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100"
onClick={() => handleOpenModal(type as FolderType)}
>
{type.charAt(0).toUpperCase() + type.slice(1)}
</button>
))}
</div>
)}
</div>
{/* Déconnexion */}
<Button variant="outline" size="sm" onClick={handleLogout}> <Button variant="outline" size="sm" onClick={handleLogout}>
<X className="h-4 w-4 mr-2" /> <X className="h-4 w-4 mr-2" />
Déconnexion 4NK Déconnexion 4NK
</Button> </Button>
{/* Debug PairingId */}
{!userPairingId && ( {!userPairingId && (
<Button variant="outline" size="sm" onClick={handleForceGetPairingId}> <Button variant="outline" size="sm" onClick={handleForceGetPairingId}>
<Brain className="h-4 w-4 mr-2" /> <Brain className="h-4 w-4 mr-2" />
Debug PairingId Debug PairingId
</Button> </Button>
)} )}
</div> </>
) : ( ) : (
<Button variant="outline" size="sm" onClick={handleLogin}> <Button variant="outline" size="sm" onClick={handleLogin}>
<Shield className="h-4 w-4 mr-2" /> <Shield className="h-4 w-4 mr-2" />
@ -1622,6 +1581,7 @@ export default function FoldersPage() {
{sortOrder === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />} {sortOrder === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
</Button> </Button>
</div> </div>
{/* Vue grille supprimée: forcer la vue liste uniquement */} {/* Vue grille supprimée: forcer la vue liste uniquement */}
</div> </div>
</div> </div>
@ -1811,7 +1771,7 @@ export default function FoldersPage() {
)} )}
{/* Folders List/Grid */} {/* Folders List/Grid */}
{/* <Card> <Card>
<CardContent className="p-0"> <CardContent className="p-0">
{viewMode === "list" ? ( {viewMode === "list" ? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -1887,8 +1847,9 @@ export default function FoldersPage() {
{filteredFolders.map((folder) => ( {filteredFolders.map((folder) => (
<div <div
key={folder.id} key={folder.id}
className={`relative group border rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer ${selectedFolders.includes(folder.id) ? "bg-blue-50 border-blue-200" : "bg-white" className={`relative group border rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer ${
}`} selectedFolders.includes(folder.id) ? "bg-blue-50 border-blue-200" : "bg-white"
}`}
onClick={() => handleOpenFolder(folder)} onClick={() => handleOpenFolder(folder)}
> >
<div className="absolute top-4 left-4" onClick={(e) => e.stopPropagation()}> <div className="absolute top-4 left-4" onClick={(e) => e.stopPropagation()}>
@ -1987,7 +1948,7 @@ export default function FoldersPage() {
</div> </div>
</div> </div>
Recent Activity {/* Recent Activity */}
<div className="mt-4 pt-4 border-t"> <div className="mt-4 pt-4 border-t">
<h4 className="text-xs font-medium text-gray-700 mb-2">Activité récente</h4> <h4 className="text-xs font-medium text-gray-700 mb-2">Activité récente</h4>
<div className="space-y-1"> <div className="space-y-1">
@ -2015,25 +1976,13 @@ export default function FoldersPage() {
? "Essayez de modifier vos critères de recherche" ? "Essayez de modifier vos critères de recherche"
: "Commencez par créer votre premier dossier"} : "Commencez par créer votre premier dossier"}
</p> </p>
<Button onClick={handleCreateFolder}>
<FolderPlus className="h-4 w-4 mr-2" />
Nouveau dossier
</Button>
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> */}
{/* ProcessesViewer Card */}
<Card className="mt-6">
<CardContent className="p-4">
<h3 className="text-lg font-semibold text-gray-900 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> </Card>
{/* Modals */} {/* Modals */}
@ -2599,16 +2548,13 @@ export default function FoldersPage() {
</div> </div>
)} )}
{/* Modal */} {/* Folder Creation Modal */}
{folderType && ( <FolderModal
<FolderModal isOpen={showCreateFolderModal}
isOpen={isModalOpen} onClose={handleCancelCreateFolder}
onClose={handleCloseModal} onSave={handleSaveNewFolder}
onSave={handleSaveNewFolder} onCancel={handleCancelCreateFolder}
onCancel={handleCloseModal} />
folderType={folderType}
/>
)}
{/* 4NK Authentication Modal */} {/* 4NK Authentication Modal */}
<AuthModal <AuthModal

View File

@ -29,7 +29,7 @@ import MessageBus from "@/lib/4nk/MessageBus"
import UserStore from "@/lib/4nk/UserStore" import UserStore from "@/lib/4nk/UserStore"
import Iframe from "@/components/4nk/Iframe" import Iframe from "@/components/4nk/Iframe"
import EventBus from "@/lib/4nk/EventBus" import EventBus from "@/lib/4nk/EventBus"
import { iframeUrl } from "../page" // DebugInfo supprimé
export default function DashboardLayout({ children }: { children: React.ReactNode }) { export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false)
@ -46,6 +46,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
const navigation = [ const navigation = [
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard }, { name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard },
@ -84,7 +85,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
messageBus.isReady().then(() => { messageBus.isReady().then(() => {
messageBus.getMyProcesses().then((res: string[]) => { messageBus.getMyProcesses().then((res: string[]) => {
setMyProcesses(res); setMyProcesses(res);
console.log("getMyProcesses", res);
}) })
}); });
} }
@ -168,6 +168,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<Shield className="h-12 w-12 mx-auto mb-4 text-blue-600 animate-pulse" /> <Shield className="h-12 w-12 mx-auto mb-4 text-blue-600 animate-pulse" />
<p className="text-gray-600">Vérification de l'authentification...</p> <p className="text-gray-600">Vérification de l'authentification...</p>
</div> </div>
{<Iframe iframeUrl={iframeUrl} />}
</div> </div>
) )
} }
@ -349,7 +351,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</div> </div>
)} )}
{isConnected && <Iframe iframeUrl={iframeUrl} />} {<Iframe iframeUrl={iframeUrl} />}
{/* Debug info retiré */} {/* Debug info retiré */}
</div> </div>

View File

@ -26,6 +26,7 @@ import {
HardDrive, HardDrive,
X, X,
} from "lucide-react" } from "lucide-react"
import MessageBus from "@/lib/4nk/MessageBus"
import Link from "next/link" import Link from "next/link"
export default function DashboardPage() { export default function DashboardPage() {
@ -53,6 +54,11 @@ export default function DashboardPage() {
const [notifications, setNotifications] = useState<any[]>([]) const [notifications, setNotifications] = useState<any[]>([])
useEffect(() => { useEffect(() => {
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
const messageBus = MessageBus.getInstance(iframeUrl)
// const mockMode = messageBus.isInMockMode()
// setIsMockMode(mockMode)
// Simuler le chargement des données // Simuler le chargement des données
if (true) { if (true) {
setStats({ setStats({

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import Link from "next/link" import Link from "next/link"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@ -8,25 +8,16 @@ import { Badge } from "@/components/ui/badge"
import { Shield, ArrowRight, Key, Zap, Users, Globe, Database, Code, CheckCircle } from "lucide-react" import { Shield, ArrowRight, Key, Zap, Users, Globe, Database, Code, CheckCircle } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import AuthModal from "@/components/4nk/AuthModal" import AuthModal from "@/components/4nk/AuthModal"
import Iframe from "@/components/4nk/Iframe"
export const iframeUrl = 'https://dev3.4nkweb.com' import UserStore from "@/lib/4nk/UserStore"
export default function HomePage() { export default function HomePage() {
const [showAuthModal, setShowAuthModal] = useState(false) const [showLoginModal, setShowLoginModal] = useState(false)
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
const [userPairingId, setUserPairingId] = useState<string | null>(null)
const handleAuthConnect = useCallback(() => {
setIsConnected(true);
setShowAuthModal(false);
router.push("/dashboard")
console.log('Auth Connect - Connexion établie, le useEffect se chargera de récupérer le userPairingId');
}, []);
const handleAuthClose = useCallback(() => {
setShowAuthModal(false);
}, []);
const router = useRouter() const router = useRouter()
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50"> <div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
@ -53,7 +44,7 @@ export default function HomePage() {
<Link href="/formation"> <Link href="/formation">
<Button variant="outline">Formation</Button> <Button variant="outline">Formation</Button>
</Link> </Link>
<Button onClick={() => setShowAuthModal(true)}>Connexion</Button> <Button onClick={() => setShowLoginModal(true)}>Connexion</Button>
</nav> </nav>
</div> </div>
</header> </header>
@ -85,15 +76,20 @@ export default function HomePage() {
</section> </section>
{/* Modal dauthentification */} {/* Modal dauthentification */}
{showAuthModal && ( {showLoginModal && (
<AuthModal <AuthModal
isOpen={showAuthModal} isOpen={showLoginModal}
onConnect={handleAuthConnect} onConnect={() => {
onClose={handleAuthClose} setShowLoginModal(false)
router.push("/dashboard") // ✅ redirection après login
}}
onClose={() => setShowLoginModal(false)}
iframeUrl={iframeUrl} iframeUrl={iframeUrl}
/> />
)} )}
{<Iframe iframeUrl={iframeUrl} />}
{/* Product Features */} {/* Product Features */}
<section id="produit" className="py-16 px-4 bg-white"> <section id="produit" className="py-16 px-4 bg-white">
<div className="container mx-auto"> <div className="container mx-auto">

View File

@ -2,7 +2,7 @@ import { useState, useEffect, memo } from 'react';
import Iframe from './Iframe'; import Iframe from './Iframe';
import MessageBus from '@/lib/4nk/MessageBus'; import MessageBus from '@/lib/4nk/MessageBus';
import Loader from '@/lib/4nk/Loader'; import Loader from '@/lib/4nk/Loader';
import Modal from '../ui/modal/Modal'; import Modal from '../modal/Modal';
interface AuthModalProps { interface AuthModalProps {
isOpen: boolean; isOpen: boolean;
@ -50,65 +50,50 @@ function AuthModal({ isOpen, onConnect, onClose, iframeUrl }: AuthModalProps) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
title="Authentification 4nk" title='Authentification 4nk'
size="md"
>
{/* Loader affiché tant que l'iframe n'est pas prête */}
{!isIframeReady && !authSuccess && (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "400px",
gap: 16,
}}
> >
<Loader width={40} /> {!isIframeReady && (
<div style={{ fontWeight: 600, fontSize: 18 }}> <div style={{
Chargement de l'authentification... display: 'flex',
</div> flexDirection: 'column',
</div> alignItems: 'center',
)} justifyContent: 'center',
height: '400px',
{/* Message de succès */} gap: 16
{authSuccess && ( }}>
<div <Loader width={40} />
style={{ <div style={{ fontWeight: 600, fontSize: 18 }}>Chargement de l'authentification...</div>
display: "flex", </div>
flexDirection: "column", )}
alignItems: "center", {authSuccess ? (
justifyContent: "center", <div style={{
height: "400px", display: 'flex',
gap: 20, flexDirection: 'column',
animation: "fadeInSuccess 0.4s ease-out", alignItems: 'center',
}} justifyContent: 'center',
> height: '400px',
<div style={{ fontWeight: 600, fontSize: 18, color: "#43a047" }}> gap: 20
Authentification réussie ! }}>
</div> <div style={{ fontWeight: 600, fontSize: 18, color: '#43a047' }}>
</div> Authentification réussie !
)} </div>
</div>
{/* Iframe affichée uniquement si dispo */} ) : (
{!authSuccess && ( <div style={{
<div display: showIframe ? 'flex' : 'none',
style={{ justifyContent: 'center',
display: showIframe ? "flex" : "none", alignItems: 'center',
justifyContent: "center", width: '100%'
alignItems: "center", }}>
width: "100%", <Iframe
minHeight: "400px", iframeUrl={iframeUrl}
}} showIframe={showIframe}
> />
<Iframe iframeUrl={iframeUrl} showIframe={showIframe} /> </div>
</div> )}
)} </Modal>
</Modal>
); );
} }

View File

@ -1,20 +1,17 @@
/* Container */ /* Folder Modal Styles */
.folder-container { .folder-container {
padding: 1.5rem; padding: 1.5rem;
max-height: 70vh; max-height: 70vh;
overflow-y: auto; overflow-y: auto;
background-color: #ffffff;
border-radius: 0.75rem;
} }
/* Form */
.folder-form { .folder-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
} }
/* Section */ /* Form Sections */
.form-section { .form-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -24,13 +21,13 @@
.section-title { .section-title {
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 600; font-weight: 600;
color: #111827; color: #374151;
margin: 0; margin: 0 0 0.5rem 0;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
border-bottom: 2px solid #e5e7eb; border-bottom: 2px solid #e5e7eb;
} }
/* Layout */ /* Form Layout */
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -40,44 +37,43 @@
.form-field { .form-field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.4rem; gap: 0.5rem;
} }
.form-field label { .form-field label {
font-size: 0.85rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: #374151; color: #374151;
} }
.required { .required {
color: #dc2626; color: #dc2626;
margin-left: 0.25rem;
} }
/* Inputs */ /* Form Inputs */
.form-field input, .form-field input,
.form-field textarea, .form-field textarea,
.form-field select { .form-field select {
padding: 0.75rem; padding: 0.75rem;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.9rem; font-size: 0.875rem;
transition: border-color 0.2s, box-shadow 0.2s; transition: border-color 0.2s, box-shadow 0.2s;
background-color: #fff; background-color: white;
} }
.form-field input:focus, .form-field input:focus,
.form-field textarea:focus, .form-field textarea:focus,
.form-field select:focus { .form-field select:focus {
outline: none; outline: none;
border-color: #2563eb; border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
.form-field input:disabled, .form-field input:disabled,
.form-field textarea:disabled, .form-field textarea:disabled,
.form-field select:disabled { .form-field select:disabled {
background-color: #f3f4f6; background-color: #f9fafb;
color: #6b7280; color: #6b7280;
cursor: not-allowed; cursor: not-allowed;
} }
@ -87,7 +83,7 @@
color: #9ca3af; color: #9ca3af;
} }
/* Tags */ /* Tag System */
.tag-list { .tag-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -98,25 +94,24 @@
.tag-item { .tag-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.5rem;
background-color: #f0f9ff; background-color: #eff6ff;
color: #0369a1; color: #1d4ed8;
padding: 0.35rem 0.75rem; padding: 0.375rem 0.75rem;
border-radius: 9999px; border-radius: 9999px;
font-size: 0.85rem; font-size: 0.875rem;
border: 1px solid #bae6fd; border: 1px solid #bfdbfe;
font-weight: 500;
} }
.tag-remove { .tag-remove {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 1.1rem; width: 1.25rem;
height: 1.1rem; height: 1.25rem;
border: none; border: none;
background: none; background: none;
color: #0369a1; color: #1d4ed8;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
font-size: 1rem; font-size: 1rem;
@ -125,7 +120,7 @@
} }
.tag-remove:hover { .tag-remove:hover {
background-color: #e0f2fe; background-color: #dbeafe;
} }
.tag-input-container { .tag-input-container {
@ -139,12 +134,12 @@
} }
.btn-add-tag { .btn-add-tag {
padding: 0.6rem 1rem; padding: 0.75rem 1rem;
background-color: #2563eb; background-color: #3b82f6;
color: white; color: white;
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.85rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
@ -152,10 +147,10 @@
} }
.btn-add-tag:hover { .btn-add-tag:hover {
background-color: #1d4ed8; background-color: #2563eb;
} }
/* Actions */ /* Form Actions */
.form-actions { .form-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -165,21 +160,16 @@
margin-top: 1rem; margin-top: 1rem;
} }
.btn-cancel,
.btn-submit {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
/* Cancel button */
.btn-cancel { .btn-cancel {
padding: 0.75rem 1.5rem;
background-color: white; background-color: white;
color: #374151; color: #374151;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
} }
.btn-cancel:hover { .btn-cancel:hover {
@ -187,15 +177,20 @@
border-color: #9ca3af; border-color: #9ca3af;
} }
/* Submit button */
.btn-submit { .btn-submit {
background-color: #10b981; padding: 0.75rem 1.5rem;
background-color: #059669;
color: white; color: white;
border: none; border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
} }
.btn-submit:hover { .btn-submit:hover {
background-color: #059669; background-color: #047857;
} }
.btn-submit:disabled { .btn-submit:disabled {
@ -203,7 +198,7 @@
cursor: not-allowed; cursor: not-allowed;
} }
/* Responsive */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.folder-container { .folder-container {
padding: 1rem; padding: 1rem;
@ -229,7 +224,7 @@
} }
} }
/* Scrollbar */ /* Custom scrollbar for the container */
.folder-container::-webkit-scrollbar { .folder-container::-webkit-scrollbar {
width: 6px; width: 6px;
} }
@ -247,101 +242,3 @@
.folder-container::-webkit-scrollbar-thumb:hover { .folder-container::-webkit-scrollbar-thumb:hover {
background: #94a3b8; background: #94a3b8;
} }
/* --- Thèmes par type de dossier --- */
/* Contrats */
.folder-contrat .section-title {
border-bottom-color: #2563eb;
}
.folder-contrat .tag-item {
background-color: #eff6ff;
color: #2563eb;
border-color: #bfdbfe;
}
.folder-contrat .btn-submit {
background-color: #2563eb;
}
.folder-contrat .btn-submit:hover {
background-color: #1d4ed8;
}
/* Projets */
.folder-projet .section-title {
border-bottom-color: #059669;
}
.folder-projet .tag-item {
background-color: #ecfdf5;
color: #059669;
border-color: #a7f3d0;
}
.folder-projet .btn-submit {
background-color: #059669;
}
.folder-projet .btn-submit:hover {
background-color: #047857;
}
/* Rapports */
.folder-rapport .section-title {
border-bottom-color: #7c3aed;
}
.folder-rapport .tag-item {
background-color: #f5f3ff;
color: #7c3aed;
border-color: #ddd6fe;
}
.folder-rapport .btn-submit {
background-color: #7c3aed;
}
.folder-rapport .btn-submit:hover {
background-color: #6d28d9;
}
/* Finance */
.folder-finance .section-title {
border-bottom-color: #d97706;
}
.folder-finance .tag-item {
background-color: #fffbeb;
color: #d97706;
border-color: #fcd34d;
}
.folder-finance .btn-submit {
background-color: #d97706;
}
.folder-finance .btn-submit:hover {
background-color: #b45309;
}
/* Ressources Humaines */
.folder-rh .section-title {
border-bottom-color: #db2777;
}
.folder-rh .tag-item {
background-color: #fdf2f8;
color: #db2777;
border-color: #f9a8d4;
}
.folder-rh .btn-submit {
background-color: #db2777;
}
.folder-rh .btn-submit:hover {
background-color: #be185d;
}
/* Marketing */
.folder-marketing .section-title {
border-bottom-color: #4f46e5;
}
.folder-marketing .tag-item {
background-color: #eef2ff;
color: #4f46e5;
border-color: #c7d2fe;
}
.folder-marketing .btn-submit {
background-color: #4f46e5;
}
.folder-marketing .btn-submit:hover {
background-color: #3730a3;
}

View File

@ -1,10 +1,8 @@
import React, { useEffect, useState, memo } from 'react'; import React, { useState, memo } from 'react';
import Modal from './ui/modal/Modal'; import Modal from './ui/modal/Modal';
import './FolderModal.css'; import './FolderModal.css';
import type { FolderData } from '../lib/4nk/models/FolderData'; import type { FolderData } from '../lib/4nk/models/FolderData';
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
interface FolderModalProps { interface FolderModalProps {
folder?: FolderData; folder?: FolderData;
onSave?: (folderData: FolderData) => void; onSave?: (folderData: FolderData) => void;
@ -12,11 +10,6 @@ interface FolderModalProps {
readOnly?: boolean; readOnly?: boolean;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
folderType?: FolderType;
renderExtraFields?: (
folderData: FolderData,
setFolderData: React.Dispatch<React.SetStateAction<FolderData>>
) => React.ReactNode;
} }
const defaultFolder: FolderData = { const defaultFolder: FolderData = {
@ -34,92 +27,80 @@ const defaultFolder: FolderData = {
stakeholders: [] stakeholders: []
}; };
function capitalize(s?: string) {
if (!s) return '';
return s.charAt(0).toUpperCase() + s.slice(1);
}
function FolderModal({ function FolderModal({
folder = defaultFolder, folder = defaultFolder,
onSave, onSave,
onCancel, onCancel,
readOnly = false, readOnly = false,
isOpen, isOpen,
onClose, onClose
folderType = 'autre',
renderExtraFields
}: FolderModalProps) { }: FolderModalProps) {
const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder }); const [folderData, setFolderData] = useState<FolderData>(folder);
const [currentCustomer, setCurrentCustomer] = useState<string>(''); const [currentCustomer, setCurrentCustomer] = useState<string>('');
const [currentStakeholder, setCurrentStakeholder] = useState<string>(''); const [currentStakeholder, setCurrentStakeholder] = useState<string>('');
const [currentNote, setCurrentNote] = useState<string>(''); const [currentNote, setCurrentNote] = useState<string>('');
// Sync when modal opens or when folder prop changes (useful pour Edit) if (!isOpen) return null;
useEffect(() => {
if (isOpen) {
// Merge with defaultFolder to ensure arrays exist
setFolderData({ ...defaultFolder, ...(folder || {}) });
setCurrentCustomer('');
setCurrentStakeholder('');
setCurrentNote('');
}
}, [isOpen, folder]);
// Generic input change handler const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target; const { name, value } = e.target;
// cast to avoid TS complaints when updating dynamic fields setFolderData(prev => ({
setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData)); ...prev,
[name]: value
}));
}; };
/* ---------- Customers ---------- */
const addCustomer = () => { const addCustomer = () => {
const v = currentCustomer.trim(); if (currentCustomer.trim() && !folderData.customers.includes(currentCustomer.trim())) {
if (!v) return; setFolderData(prev => ({
if (!Array.isArray(folderData.customers)) folderData.customers = []; ...prev,
if (!folderData.customers.includes(v)) { customers: [...prev.customers, currentCustomer.trim()]
setFolderData(prev => ({ ...prev, customers: [...(prev.customers || []), v] })); }));
setCurrentCustomer('');
} }
setCurrentCustomer('');
}; };
const removeCustomer = (customer: string) => { const removeCustomer = (customer: string) => {
setFolderData(prev => ({ ...prev, customers: (prev.customers || []).filter(c => c !== customer) })); setFolderData(prev => ({
...prev,
customers: prev.customers.filter(c => c !== customer)
}));
}; };
/* ---------- Stakeholders ---------- */
const addStakeholder = () => { const addStakeholder = () => {
const v = currentStakeholder.trim(); if (currentStakeholder.trim() && !folderData.stakeholders.includes(currentStakeholder.trim())) {
if (!v) return; setFolderData(prev => ({
if (!Array.isArray(folderData.stakeholders)) folderData.stakeholders = []; ...prev,
if (!folderData.stakeholders.includes(v)) { stakeholders: [...prev.stakeholders, currentStakeholder.trim()]
setFolderData(prev => ({ ...prev, stakeholders: [...(prev.stakeholders || []), v] })); }));
setCurrentStakeholder('');
} }
setCurrentStakeholder('');
}; };
const removeStakeholder = (stakeholder: string) => { const removeStakeholder = (stakeholder: string) => {
setFolderData(prev => ({ ...prev, stakeholders: (prev.stakeholders || []).filter(s => s !== stakeholder) })); setFolderData(prev => ({
...prev,
stakeholders: prev.stakeholders.filter(s => s !== stakeholder)
}));
}; };
/* ---------- Notes ---------- */
const addNote = () => { const addNote = () => {
const v = currentNote.trim(); if (currentNote.trim() && !folderData.notes.includes(currentNote.trim())) {
if (!v) return; setFolderData(prev => ({
if (!Array.isArray(folderData.notes)) folderData.notes = []; ...prev,
if (!folderData.notes.includes(v)) { notes: [...prev.notes, currentNote.trim()]
setFolderData(prev => ({ ...prev, notes: [...(prev.notes || []), v] })); }));
setCurrentNote('');
} }
setCurrentNote('');
}; };
const removeNote = (note: string) => { const removeNote = (note: string) => {
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) })); setFolderData(prev => ({
...prev,
notes: prev.notes.filter(m => m !== note)
}));
}; };
/* ---------- Submit / Cancel ---------- */
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (onSave) { if (onSave) {
@ -128,32 +109,22 @@ function FolderModal({
updated_at: new Date().toISOString() updated_at: new Date().toISOString()
}); });
} }
if (onClose) {
onClose(); // ← ça ferme le modal après sauvegarde
}
}; };
const handleCancel = () => { const handleCancel = () => {
if (onCancel) { if (onCancel) {
onCancel(); // ton callback spécifique onCancel();
} else if (onClose) { } else {
onClose(); // fallback si pas de onCancel onClose();
} }
}; };
// Title text
const title = `Créer un dossier ${capitalize(folderType)}`;
return ( return (
// On ne fait PAS "if (!isOpen) return null" : Modal gère l'animation/visibilité <Modal isOpen={isOpen} onClose={onClose} title="Créer un nouveau dossier" size="lg">
<Modal isOpen={isOpen} onClose={onClose} title={title} size="lg"> <div className="folder-container">
<div className={`folder-container folder-${folderType}`}>
<form className="folder-form" onSubmit={handleSubmit}> <form className="folder-form" onSubmit={handleSubmit}>
{/* Informations principales */}
<div className="form-section"> <div className="form-section">
<h3 className="section-title">Informations principales</h3> <h3 className="section-title">Informations principales</h3>
<div className="form-row"> <div className="form-row">
<div className="form-field"> <div className="form-field">
<label> <label>
@ -162,14 +133,13 @@ function FolderModal({
<input <input
type="text" type="text"
name="folderNumber" name="folderNumber"
value={folderData.folderNumber || ''} value={folderData.folderNumber}
onChange={handleInputChange} onChange={handleInputChange}
required required
disabled={readOnly} disabled={readOnly}
placeholder="ex: DOC-2025-001" placeholder="ex: DOC-2025-001"
/> />
</div> </div>
<div className="form-field"> <div className="form-field">
<label> <label>
Nom <span className="required">*</span> Nom <span className="required">*</span>
@ -177,7 +147,7 @@ function FolderModal({
<input <input
type="text" type="text"
name="name" name="name"
value={folderData.name || ''} value={folderData.name}
onChange={handleInputChange} onChange={handleInputChange}
required required
disabled={readOnly} disabled={readOnly}
@ -186,11 +156,52 @@ function FolderModal({
</div> </div>
</div> </div>
<div className="form-row">
<div className="form-field">
<label>
Type d'acte <span className="required">*</span>
</label>
<select
name="deedType"
value={folderData.deedType}
onChange={handleInputChange}
required
disabled={readOnly}
>
<option value="">Sélectionnez le type d'acte</option>
<option value="vente">Vente</option>
<option value="achat">Achat</option>
<option value="succession">Succession</option>
<option value="donation">Donation</option>
<option value="hypotheque">Hypothèque</option>
<option value="bail">Bail</option>
<option value="autre">Autre</option>
</select>
</div>
<div className="form-field">
<label>
Statut <span className="required">*</span>
</label>
<select
name="status"
value={folderData.status}
onChange={handleInputChange}
required
disabled={readOnly}
>
<option value="active">Actif</option>
<option value="pending">En attente</option>
<option value="completed">Complété</option>
<option value="archived">Archivé</option>
</select>
</div>
</div>
<div className="form-field"> <div className="form-field">
<label>Description</label> <label>Description</label>
<textarea <textarea
name="description" name="description"
value={folderData.description || ''} value={folderData.description}
onChange={handleInputChange} onChange={handleInputChange}
disabled={readOnly} disabled={readOnly}
placeholder="Description du dossier" placeholder="Description du dossier"
@ -203,7 +214,7 @@ function FolderModal({
<label>Description d'archivage</label> <label>Description d'archivage</label>
<textarea <textarea
name="archived_description" name="archived_description"
value={folderData.archived_description || ''} value={folderData.archived_description}
onChange={handleInputChange} onChange={handleInputChange}
disabled={readOnly} disabled={readOnly}
placeholder="Raison d'archivage" placeholder="Raison d'archivage"
@ -213,19 +224,100 @@ function FolderModal({
)} )}
</div> </div>
{/* Champs spécifiques injectés */} <div className="form-section">
{renderExtraFields && ( <h3 className="section-title">Clients</h3>
<div className="form-section"> <div className="tag-list">
{renderExtraFields(folderData, setFolderData)} {folderData.customers.map((customer, index) => (
<div key={index} className="tag-item">
<span>{customer}</span>
{!readOnly && (
<button
type="button"
className="tag-remove"
onClick={() => removeCustomer(customer)}
aria-label="Supprimer ce client"
>
×
</button>
)}
</div>
))}
</div> </div>
)}
{/* Notes */} {!readOnly && (
<div className="form-field tag-input-container">
<input
type="text"
value={currentCustomer}
onChange={(e) => setCurrentCustomer(e.target.value)}
placeholder="Ajouter un client"
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addCustomer();
}
}}
/>
<button
type="button"
className="btn-add-tag"
onClick={addCustomer}
>
Ajouter
</button>
</div>
)}
</div>
<div className="form-section">
<h3 className="section-title">Parties prenantes</h3>
<div className="tag-list">
{folderData.stakeholders.map((stakeholder, index) => (
<div key={index} className="tag-item">
<span>{stakeholder}</span>
{!readOnly && (
<button
type="button"
className="tag-remove"
onClick={() => removeStakeholder(stakeholder)}
aria-label="Supprimer cette partie prenante"
>
×
</button>
)}
</div>
))}
</div>
{!readOnly && (
<div className="form-field tag-input-container">
<input
type="text"
value={currentStakeholder}
onChange={(e) => setCurrentStakeholder(e.target.value)}
placeholder="Ajouter une partie prenante"
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addStakeholder();
}
}}
/>
<button
type="button"
className="btn-add-tag"
onClick={addStakeholder}
>
Ajouter
</button>
</div>
)}
</div>
<div className="form-section"> <div className="form-section">
<h3 className="section-title">Notes</h3> <h3 className="section-title">Notes</h3>
<div className="tag-list"> <div className="tag-list">
{(folderData.notes || []).map((note, index) => ( {folderData.notes.map((note, index) => (
<div key={index} className="tag-item"> <div key={index} className="tag-item">
<span>{note}</span> <span>{note}</span>
{!readOnly && ( {!readOnly && (
@ -249,7 +341,7 @@ function FolderModal({
value={currentNote} value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)} onChange={(e) => setCurrentNote(e.target.value)}
placeholder="Ajouter une note" placeholder="Ajouter une note"
onKeyDown={(e) => { onKeyPress={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
addNote(); addNote();
@ -267,16 +359,14 @@ function FolderModal({
)} )}
</div> </div>
{/* Informations système */}
<div className="form-section"> <div className="form-section">
<h3 className="section-title">Informations système</h3> <h3 className="section-title">Informations système</h3>
<div className="form-row"> <div className="form-row">
<div className="form-field"> <div className="form-field">
<label>Créé le</label> <label>Créé le</label>
<input <input
type="text" type="text"
value={new Date(folderData.created_at).toLocaleString('fr-FR', { value={new Date(folderData.created_at).toLocaleDateString('fr-FR', {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
@ -287,12 +377,11 @@ function FolderModal({
readOnly readOnly
/> />
</div> </div>
<div className="form-field"> <div className="form-field">
<label>Dernière mise à jour</label> <label>Dernière mise à jour</label>
<input <input
type="text" type="text"
value={new Date(folderData.updated_at).toLocaleString('fr-FR', { value={new Date(folderData.updated_at).toLocaleDateString('fr-FR', {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
@ -306,12 +395,19 @@ function FolderModal({
</div> </div>
</div> </div>
{/* Actions */}
<div className="form-actions"> <div className="form-actions">
<button type="button" className="btn-cancel" onClick={handleCancel}> <button
type="button"
className="btn-cancel"
onClick={handleCancel}
>
Annuler Annuler
</button> </button>
<button type="submit" className="btn-submit" disabled={readOnly}> <button
type="submit"
className="btn-submit"
disabled={readOnly}
>
Enregistrer Enregistrer
</button> </button>
</div> </div>
@ -319,7 +415,7 @@ function FolderModal({
</div> </div>
</Modal> </Modal>
); );
} };
FolderModal.displayName = 'FolderModal'; FolderModal.displayName = 'FolderModal';
export default memo(FolderModal); export default memo(FolderModal);

View File

@ -1,325 +0,0 @@
import { useState, memo } from 'react';
import { isFileBlob, type FileBlob } from '@/lib/4nk/models/Data';
import { iframeUrl } from "@/app/page";
import MessageBus from '@/lib/4nk/MessageBus';
interface BlockState {
commited_in: string;
state_id: string;
pcd_commitment: Record<string, string>;
public_data: Record<string, any>;
}
interface Block {
states: BlockState[];
}
interface Processes {
[key: string]: Block;
}
interface ProcessesViewerProps {
processes: Processes | null;
myProcesses: string[];
onProcessesUpdate?: (processes: Processes) => void;
}
const compareStates = (
currentState: BlockState,
index: number,
previousState?: BlockState,
currentPrivateData?: Record<string, any>,
previousPrivateData?: Record<string, any>
) => {
const result: Record<string, {
value: any,
status: 'unchanged' | 'modified',
hash?: string,
isPrivate: boolean,
stateId: string
}> = {};
Object.keys(currentState.public_data).forEach(key => {
const currentValue = currentState.public_data[key];
const previousValue = previousState?.public_data[key];
const isModified = index > 0 && previousValue !== undefined && JSON.stringify(currentValue) !== JSON.stringify(previousValue);
result[key] = {
value: currentValue,
status: isModified ? 'modified' : 'unchanged',
hash: currentState.pcd_commitment[key],
isPrivate: false,
stateId: currentState.state_id
};
});
if (index === 0 && currentPrivateData) {
Object.entries(currentPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'unchanged',
hash: currentState.pcd_commitment[key],
isPrivate: true,
stateId: currentState.state_id
};
});
} else if (previousPrivateData) {
Object.entries(previousPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'unchanged',
hash: previousState?.pcd_commitment[key],
isPrivate: true,
stateId: previousState!.state_id
};
});
if (currentPrivateData) {
Object.entries(currentPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'modified',
hash: currentState.pcd_commitment[key],
isPrivate: true,
stateId: currentState.state_id
};
});
}
}
return result;
};
function ProcessesViewer({ processes, myProcesses, onProcessesUpdate }: ProcessesViewerProps) {
const [expandedBlocks, setExpandedBlocks] = useState<string[]>([]);
const [isFiltered, setIsFiltered] = useState<boolean>(false);
const [privateData, setPrivateData] = useState<Record<string, Record<string, any>>>({});
const [editingField, setEditingField] = useState<{processId: string; stateId: string; key: string; value: any;} | null>(null);
const [tempValue, setTempValue] = useState<any>(null);
const toggleBlock = (blockId: string) => {
setExpandedBlocks(prev => prev.includes(blockId) ? prev.filter(id => id !== blockId) : [...prev, blockId]);
};
const handleFilterClick = () => setIsFiltered(prev => !prev);
if (!processes || Object.keys(processes).length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<h3 className="text-lg font-medium mb-2">Aucun processus disponible</h3>
<p>Connectez-vous pour voir vos processus</p>
</div>
);
}
const fetchPrivateData = async (processId: string, stateId: string) => {
if (!expandedBlocks.includes(processId) || !myProcesses.includes(processId)) return;
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const data = await messageBus.getData(processId, stateId);
setPrivateData(prev => ({ ...prev, [stateId]: data }));
} catch (err) {
console.error(err);
}
};
const handleDownload = (name: string | undefined, fileBlob: FileBlob) => {
const blob = new Blob([fileBlob.data], { type: fileBlob.type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name || 'download';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const formatValue = (key: string, value: string | number[] | FileBlob) => {
if (isFileBlob(value)) {
return (
<button className="text-blue-600 hover:underline" onClick={() => handleDownload(key, value)}>
📥 Télécharger
</button>
);
}
return <span>{JSON.stringify(value || '')}</span>;
};
const getDataIcon = (value: any) => {
if (isFileBlob(value)) return '📄';
if (typeof value === 'string') return '📝';
if (typeof value === 'number') return '🔢';
if (Array.isArray(value)) return '📋';
if (typeof value === 'boolean') return '✅';
return '📦';
};
const handleFieldUpdate = async (processId: string, stateId: string, key: string, value: any) => {
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const updatedProcess = await messageBus.updateProcess(processId, stateId, { [key]: value }, [], null);
if (!updatedProcess) throw new Error('No updated process found');
const newStateId = updatedProcess.diffs[0]?.state_id;
if (!newStateId) throw new Error('No new state id found');
await messageBus.notifyProcessUpdate(processId, newStateId);
await messageBus.validateState(processId, newStateId);
const updatedProcesses = await messageBus.getProcesses();
onProcessesUpdate?.(updatedProcesses);
} catch (err) {
console.error(err);
}
};
const renderEditForm = (key: string, value: any, onSave: (v: any) => void, onCancel: () => void) => {
if (tempValue === null) setTempValue(value);
const handleFormClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); };
if (isFileBlob(value)) {
return (
<div className="flex flex-col space-y-2" onClick={handleFormClick}>
<input type="file" onChange={(e) => {
e.stopPropagation();
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setTempValue({ type: file.type, data: new Uint8Array(event.target.result as ArrayBuffer) });
}
};
reader.readAsArrayBuffer(file);
}
}}/>
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => { onSave(tempValue); setTempValue(null); }}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => { onCancel(); setTempValue(null); }}>Annuler</button>
</div>
</div>
);
}
if (typeof value === 'boolean') {
return (
<div className="flex items-center space-x-2" onClick={handleFormClick}>
<select className="border rounded px-2 py-1" value={tempValue.toString()} onChange={(e) => setTempValue(e.target.value === 'true')}>
<option value="true">Vrai</option>
<option value="false">Faux</option>
</select>
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => onCancel()}>Annuler</button>
</div>
</div>
);
}
if (Array.isArray(value)) {
return (
<div className="flex flex-col space-y-2" onClick={handleFormClick}>
<textarea className="border rounded p-2" rows={4} value={JSON.stringify(tempValue, null, 2)} onChange={(e) => {
try { const parsed = JSON.parse(e.target.value); if (Array.isArray(parsed)) setTempValue(parsed); } catch {}
}}/>
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => onCancel()}>Annuler</button>
</div>
</div>
);
}
return (
<div className="flex space-x-2 items-center" onClick={handleFormClick}>
<input className="border rounded px-2 py-1" type={typeof value === 'number' ? 'number' : 'text'} value={tempValue} onChange={(e) => setTempValue(typeof value === 'number' ? parseFloat(e.target.value) : e.target.value)} autoFocus />
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 rounded" onClick={() => onCancel()}>Annuler</button>
</div>
</div>
);
};
const renderDataField = (key: string, value: any, hash: string | undefined, isPrivate: boolean, processId: string, stateId: string, status: 'unchanged' | 'modified' = 'unchanged', originStateId?: string) => {
const isEditing = editingField?.key === key && editingField?.processId === processId && editingField?.stateId === stateId;
return (
<div key={key} className={`border rounded p-2 mb-2 transition-colors ${status === 'modified' ? 'bg-green-100' : 'bg-white'}`}>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center space-x-1">
<span title={isPrivate ? 'Donnée privée' : 'Donnée publique'}>{isPrivate ? '🔒' : '🌐'}</span>
<span>{getDataIcon(value)}</span>
<span className="font-medium">{key}</span>
{originStateId && originStateId !== stateId && <span title={`Propagé depuis l'état ${originStateId}`}></span>}
</div>
<button className="text-sm text-gray-500 hover:text-gray-700" onClick={(e) => { e.stopPropagation(); setEditingField({ processId, stateId, key, value }); }}>
{isEditing ? '✕' : '🔄'}
</button>
</div>
<div>
{isEditing ? renderEditForm(key, value, async (v) => { await handleFieldUpdate(processId, stateId, key, v); setEditingField(null); setTempValue(null); }, () => { setEditingField(null); setTempValue(null); }) : (
<div className="flex items-center space-x-1">
<span>{formatValue(key, value)}</span>
{hash && <span title={`Hash: ${hash}`}>🔑</span>}
</div>
)}
</div>
</div>
);
};
return (
<div className="w-full h-full overflow-auto p-2">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Processus</h2>
<button className="px-2 py-1 border rounded text-sm" onClick={handleFilterClick}>{isFiltered ? 'Show All' : 'Filter'}</button>
</div>
<p className="mb-2 text-sm text-gray-500">{isFiltered ? Object.keys(processes).filter(p => myProcesses.includes(p)).length : Object.keys(processes).length} processus disponible(s)</p>
<div className="space-y-4">
{Object.entries(processes).map(([processId, process]) => {
if (isFiltered && !myProcesses.includes(processId)) return null;
const isExpanded = expandedBlocks.includes(processId);
const stateCount = process.states.length - 1;
return (
<div key={processId} className="border rounded shadow-sm">
<div className="flex justify-between items-center p-2 cursor-pointer bg-gray-50" onClick={() => toggleBlock(processId)}>
<div className="font-mono">{processId.slice(0,8)}...{processId.slice(-4)}</div>
<div>{stateCount} état(s)</div>
<div>{isExpanded ? '▼' : '▶'}</div>
</div>
{isExpanded && (
<div className="p-2 space-y-2 bg-white">
<div><strong>Process ID:</strong> {processId}</div>
{process.states.map((state, index) => {
if (index === stateCount) return null;
if (myProcesses.includes(processId) && !privateData[state.state_id]) setTimeout(() => fetchPrivateData(processId, state.state_id), 0);
const statePrivateData = privateData[state.state_id] || {};
const stateData = compareStates(state, index, index > 0 ? process.states[index-1] : undefined, statePrivateData, index > 0 ? privateData[process.states[index-1].state_id] : undefined);
return (
<div key={`${processId}-state-${index}`} className="border-t pt-2">
<h4 className="font-medium mb-1">État {index+1}</h4>
<div className="text-sm mb-1"><strong>TransactionId:</strong> {state.commited_in}</div>
<div className="text-sm mb-2"><strong>Empreinte totale de l'état:</strong> {state.state_id}</div>
<div className="space-y-1">
{Object.entries(stateData).map(([key, { value, status, hash, isPrivate, stateId }]) => renderDataField(key, value, hash, isPrivate, processId, stateId, status, state.state_id))}
{myProcesses.includes(processId) && Object.keys(statePrivateData).length === 0 && <div className="text-gray-400 text-sm">Chargement des données privées...</div>}
{!myProcesses.includes(processId) && <div className="text-gray-400 text-sm">🔒 Vous n'avez pas accès aux données privées de ce processus</div>}
</div>
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
ProcessesViewer.displayName = 'ProcessesViewer';
export default memo(ProcessesViewer);

163
components/modal/Modal.css Normal file
View File

@ -0,0 +1,163 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(35, 36, 42, 0.82);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: modal-fadein 0.33s cubic-bezier(.4, 0, .2, 1);
backdrop-filter: blur(3.5px);
-webkit-backdrop-filter: blur(3.5px);
}
@keyframes modal-fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-container {
background: #23242a;
border-radius: 18px;
min-width: 340px;
max-width: 95vw;
min-height: 0;
padding: 0 0 24px 0;
position: relative;
box-shadow: 0 12px 48px 0 rgba(0, 0, 0, 0.34), 0 2px 12px 0 rgba(30, 34, 44, 0.10);
overflow: hidden;
animation: modal-popin 0.34s cubic-bezier(.4, 0, .2, 1);
transition: box-shadow 0.2s, opacity 0.25s cubic-bezier(.4, 0, .2, 1);
}
.modal-container.modal-closing {
opacity: 0;
transform: translateY(32px) scale(0.97);
pointer-events: none;
}
@keyframes modal-popin {
from {
opacity: 0;
transform: translateY(32px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
background: linear-gradient(90deg, #23242a 85%, #23242aEE 100%);
color: #fff;
padding: 22px 30px 14px 30px;
border-radius: 18px 18px 0 0;
box-shadow: 0 2px 12px 0 rgba(30, 34, 44, 0.06);
position: relative;
display: flex;
align-items: center;
min-height: 52px;
}
.modal-header h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
letter-spacing: 0.01em;
color: #fff;
}
.modal-close {
position: absolute;
top: 10px;
right: 16px;
background: transparent;
border: none;
font-size: 2rem;
color: #e3e4e8;
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.18s, color 0.18s;
z-index: 2;
border-radius: 6px;
padding: 0;
}
.modal-close svg {
display: block;
width: 24px;
height: 24px;
background: none;
pointer-events: none;
}
.modal-close:hover,
.modal-close:focus {
background: rgba(255, 255, 255, 0.10);
color: #fff;
}
.modal-close:active {
background: rgba(67, 160, 71, 0.13);
}
.modal-close:active {
background: rgba(67, 160, 71, 0.13);
}
.modal-body {
padding: 28px 28px 0 28px;
max-height: 70vh;
overflow-y: auto;
color: #e3e4e8;
font-size: 1rem;
}
@media (max-width: 600px) {
.modal-container {
min-width: 0;
width: 98vw;
padding: 0 0 12px 0;
border-radius: 12px;
}
.modal-header {
padding: 16px 10px 10px 14px;
border-radius: 12px 12px 0 0;
}
.modal-body {
padding: 14px 8px 0 8px;
}
.modal-close {
top: 6px;
right: 6px;
width: 30px;
height: 30px;
font-size: 1.2rem;
}
}
.modal-body {
width: 100%;
}

View File

@ -0,0 +1,38 @@
import React, { memo } from 'react';
import './Modal.css';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps) {
if (!isOpen) {
return null;
}
return (
<div className="modal-overlay modal-fadein">
<div className="modal-container modal-popin">
<button className="close-button modal-close" onClick={onClose} aria-label="Fermer">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M6 6L18 18M18 6L6 18" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" filter="url(#shadow)" />
<defs>
<filter id="shadow" x="-2" y="-2" width="28" height="28" filterUnits="userSpaceOnUse">
<feDropShadow dx="0" dy="0" stdDeviation="1.2" floodColor="#23242a" />
</filter>
</defs>
</svg>
</button>
{title && <div className="modal-header modal-header"><h2>{title}</h2></div>}
<div className="modal-body modal-body">
{children}
</div>
</div>
</div>
);
}
Modal.displayName = 'Modal';
export default memo(Modal);

View File

@ -1,101 +1,104 @@
/* Overlay */ /* Modal Overlay */
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
inset: 0; top: 0;
background-color: rgba(17, 24, 39, 0.55); left: 0;
backdrop-filter: blur(6px); right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
padding: 1rem; padding: 1rem;
transition: opacity 0.3s ease;
} }
.fade-in { opacity: 1; } /* Modal Container */
.fade-out { opacity: 0; }
/* Container */
.modal-container { .modal-container {
background: #fff; background: white;
border-radius: 1rem; border-radius: 0.75rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-height: 90vh; max-height: 90vh;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
transition: all 0.3s ease;
} }
.modal-sm { max-width: 28rem; } .modal-sm {
.modal-md { max-width: 32rem; } max-width: 28rem;
.modal-lg { max-width: 48rem; }
.modal-xl { max-width: 64rem; }
.slide-in {
transform: translateY(0);
opacity: 1;
} }
.slide-out { .modal-md {
transform: translateY(20px); max-width: 32rem;
opacity: 0;
} }
/* Header */ .modal-lg {
max-width: 48rem;
}
.modal-xl {
max-width: 64rem;
}
/* Modal Header */
.modal-header { .modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 1.25rem 1.5rem; padding: 1.5rem;
background: linear-gradient(90deg, #f9fafb, #f3f4f6);
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
} }
.modal-title { .modal-title {
font-size: 1.3rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: #111827; color: #111827;
margin: 0; margin: 0;
} }
/* Close Button */
.modal-close-button { .modal-close-button {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 2.25rem; width: 2rem;
height: 2.25rem; height: 2rem;
border: none; border: none;
background: none; background: none;
border-radius: 0.5rem; border-radius: 0.375rem;
color: #6b7280; color: #6b7280;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.modal-close-button:hover { .modal-close-button:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: #f3f4f6;
color: #111827; color: #374151;
transform: rotate(90deg);
} }
/* Content */ /* Modal Content */
.modal-content { .modal-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 1.5rem; padding: 0;
background: #fff;
} }
@keyframes fadeInSuccess { /* Responsive */
from { @media (max-width: 640px) {
opacity: 0; .modal-overlay {
transform: scale(0.95); padding: 0.5rem;
} }
to {
opacity: 1; .modal-container {
transform: scale(1); max-height: 95vh;
} }
}
.modal-header {
padding: 1rem;
}
.modal-title {
font-size: 1.125rem;
}
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import './Modal.css'; import './Modal.css';
@ -10,26 +10,14 @@ interface ModalProps {
size?: 'sm' | 'md' | 'lg' | 'xl'; size?: 'sm' | 'md' | 'lg' | 'xl';
} }
const Modal: React.FC<ModalProps> = ({ const Modal: React.FC<ModalProps> = ({
isOpen, isOpen,
onClose, onClose,
title, title,
children, children,
size = 'md' size = 'md'
}) => { }) => {
const [isVisible, setIsVisible] = useState(isOpen); if (!isOpen) return null;
useEffect(() => {
if (isOpen) {
setIsVisible(true);
} else {
// attendre que l'animation de fermeture se joue
const timer = setTimeout(() => setIsVisible(false), 300); // durée en ms = durée CSS
return () => clearTimeout(timer);
}
}, [isOpen]);
if (!isVisible) return null;
const handleBackdropClick = (e: React.MouseEvent) => { const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
@ -38,8 +26,8 @@ const Modal: React.FC<ModalProps> = ({
}; };
return ( return (
<div className={`modal-overlay ${isOpen ? 'fade-in' : 'fade-out'}`} onClick={handleBackdropClick}> <div className="modal-overlay" onClick={handleBackdropClick}>
<div className={`modal-container modal-${size} ${isOpen ? 'slide-in' : 'slide-out'}`}> <div className={`modal-container modal-${size}`}>
<div className="modal-header"> <div className="modal-header">
<h2 className="modal-title">{title}</h2> <h2 className="modal-title">{title}</h2>
<button <button
@ -50,7 +38,9 @@ const Modal: React.FC<ModalProps> = ({
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
<div className="modal-content">{children}</div> <div className="modal-content">
{children}
</div>
</div> </div>
</div> </div>
); );

View File

@ -199,10 +199,11 @@ export default class MessageBus {
const accessToken = userStore.getAccessToken()!; const accessToken = userStore.getAccessToken()!;
const correlationId = uuidv4(); const correlationId = uuidv4();
console.log(correlationId);
this.initMessageListener(correlationId); this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => { const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => {
console.log('MessageBus - PROCESSES_RETRIEVED', processes); console.log(responseId);
if (responseId !== correlationId) { if (responseId !== correlationId) {
return; return;
} }
@ -451,7 +452,7 @@ export default class MessageBus {
this.initMessageListener(correlationId); this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, updatedProcess: any) => { const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, updatedProcess: any) => {
console.log('MessageBus - PROCESS_UPDATED', updatedProcess); console.log('PROCESS_UPDATED', updatedProcess);
if (responseId !== correlationId) { if (responseId !== correlationId) {
return; return;
} }
@ -529,7 +530,7 @@ export default class MessageBus {
this.initMessageListener(correlationId); this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => { const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => {
console.log('MessageBus - STATE_VALIDATED', updatedProcess); console.log(updatedProcess);
if (responseId !== correlationId) { if (responseId !== correlationId) {
return; return;
} }
@ -726,6 +727,7 @@ export default class MessageBus {
EventBus.getInstance().emit('ERROR_PROCESS_UPDATED', correlationId, error); EventBus.getInstance().emit('ERROR_PROCESS_UPDATED', correlationId, error);
return; return;
} }
console.log('PROCESS_UPDATED', message);
EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('PROCESS_UPDATED', correlationId, message.updatedProcess); EventBus.getInstance().emit('PROCESS_UPDATED', correlationId, message.updatedProcess);
break; break;

View File

@ -1,135 +0,0 @@
import { isFileBlob, type FileBlob } from "./Data";
import type { RoleDefinition } from "./Roles";
export interface RhData {
folderNumber: string;
name: string;
description: string;
nom_salarie: string;
poste: string;
num_secu: string;
message_private: string;
created_at: string;
updated_at: string;
types_documents: string[];
rhs: string[];
salaries: string[];
notes: string[];
}
export function isRhData(data: any): data is RhData {
if (typeof data !== 'object' || data === null) return false;
const requiredStringFields = [
'folderNumber',
'name',
'description',
'nom_salarie',
'poste',
'num_secu',
'message_private',
'created_at',
'updated_at'
];
for (const field of requiredStringFields) {
if (typeof data[field] !== 'string') return false;
}
const requiredArrayFields = [
'types_documents',
'rhs',
'salaries',
'notes',
];
for (const field of requiredArrayFields) {
if (!Array.isArray(data[field]) || !data[field].every((item: any) => typeof item === 'string')) {
return false;
}
}
return true;
}
const emptyRhData: RhData = {
folderNumber: '',
name: '',
description: '',
nom_salarie: '',
poste: '',
num_secu: '',
message_private: '',
created_at: '',
updated_at: '',
types_documents: [],
rhs: [],
salaries: [],
notes: []
};
const rhDataFields: string[] = Object.keys(emptyRhData);
const RhPublicFields: string[] = [
'nom_salarie',
'poste'
];
// All the attributes are private in that case
export const RhPrivateFields = [
...rhDataFields.filter(key => !RhPublicFields.includes(key))
];
export interface RhCreated {
processId: string,
process: any, // Process
rhData: RhData,
}
export function setDefaultRhRoles(ownerId: string, rhs: string[], salaries: string[]): Record<string, RoleDefinition> {
return {
demiurge: {
members: [ownerId],
validation_rules: [],
storages: []
},
owner: {
members: [ownerId],
validation_rules: [
{
quorum: 0.5,
fields: [...rhDataFields, 'roles'],
min_sig_member: 1,
},
],
storages: []
},
rhs: {
members: rhs,
validation_rules: [
{
quorum: 0.5,
fields: rhDataFields,
min_sig_member: 1,
},
],
storages: []
},
salaries: {
members: salaries,
validation_rules: [
{
quorum: 0.0,
fields: rhDataFields,
min_sig_member: 0.0,
},
],
storages: []
},
apophis: {
members: [ownerId],
validation_rules: [],
storages: []
}
}
};