Compare commits

...

9 Commits

14 changed files with 1041 additions and 705 deletions

View File

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

View File

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

View File

@ -26,7 +26,6 @@ import {
HardDrive,
X,
} from "lucide-react"
import MessageBus from "@/lib/4nk/MessageBus"
import Link from "next/link"
export default function DashboardPage() {
@ -54,11 +53,6 @@ export default function DashboardPage() {
const [notifications, setNotifications] = useState<any[]>([])
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
if (true) {
setStats({

View File

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

View File

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

View File

@ -1,17 +1,20 @@
/* Folder Modal Styles */
/* Container */
.folder-container {
padding: 1.5rem;
max-height: 70vh;
overflow-y: auto;
background-color: #ffffff;
border-radius: 0.75rem;
}
/* Form */
.folder-form {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Form Sections */
/* Section */
.form-section {
display: flex;
flex-direction: column;
@ -21,13 +24,13 @@
.section-title {
font-size: 1.125rem;
font-weight: 600;
color: #374151;
margin: 0 0 0.5rem 0;
color: #111827;
margin: 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e5e7eb;
}
/* Form Layout */
/* Layout */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
@ -37,43 +40,44 @@
.form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.4rem;
}
.form-field label {
font-size: 0.875rem;
font-size: 0.85rem;
font-weight: 500;
color: #374151;
}
.required {
color: #dc2626;
margin-left: 0.25rem;
}
/* Form Inputs */
/* Inputs */
.form-field input,
.form-field textarea,
.form-field select {
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
font-size: 0.9rem;
transition: border-color 0.2s, box-shadow 0.2s;
background-color: white;
background-color: #fff;
}
.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.form-field input:disabled,
.form-field textarea:disabled,
.form-field select:disabled {
background-color: #f9fafb;
background-color: #f3f4f6;
color: #6b7280;
cursor: not-allowed;
}
@ -83,7 +87,7 @@
color: #9ca3af;
}
/* Tag System */
/* Tags */
.tag-list {
display: flex;
flex-wrap: wrap;
@ -94,24 +98,25 @@
.tag-item {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: #eff6ff;
color: #1d4ed8;
padding: 0.375rem 0.75rem;
gap: 0.4rem;
background-color: #f0f9ff;
color: #0369a1;
padding: 0.35rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
border: 1px solid #bfdbfe;
font-size: 0.85rem;
border: 1px solid #bae6fd;
font-weight: 500;
}
.tag-remove {
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
width: 1.1rem;
height: 1.1rem;
border: none;
background: none;
color: #1d4ed8;
color: #0369a1;
cursor: pointer;
border-radius: 50%;
font-size: 1rem;
@ -120,7 +125,7 @@
}
.tag-remove:hover {
background-color: #dbeafe;
background-color: #e0f2fe;
}
.tag-input-container {
@ -134,12 +139,12 @@
}
.btn-add-tag {
padding: 0.75rem 1rem;
background-color: #3b82f6;
padding: 0.6rem 1rem;
background-color: #2563eb;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
@ -147,10 +152,10 @@
}
.btn-add-tag:hover {
background-color: #2563eb;
background-color: #1d4ed8;
}
/* Form Actions */
/* Actions */
.form-actions {
display: flex;
justify-content: flex-end;
@ -160,37 +165,37 @@
margin-top: 1rem;
}
.btn-cancel {
.btn-cancel,
.btn-submit {
padding: 0.75rem 1.5rem;
background-color: white;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
/* Cancel button */
.btn-cancel {
background-color: white;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-cancel:hover {
background-color: #f9fafb;
border-color: #9ca3af;
}
/* Submit button */
.btn-submit {
padding: 0.75rem 1.5rem;
background-color: #059669;
background-color: #10b981;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-submit:hover {
background-color: #047857;
background-color: #059669;
}
.btn-submit:disabled {
@ -198,7 +203,7 @@
cursor: not-allowed;
}
/* Responsive Design */
/* Responsive */
@media (max-width: 768px) {
.folder-container {
padding: 1rem;
@ -224,7 +229,7 @@
}
}
/* Custom scrollbar for the container */
/* Scrollbar */
.folder-container::-webkit-scrollbar {
width: 6px;
}
@ -242,3 +247,101 @@
.folder-container::-webkit-scrollbar-thumb:hover {
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,8 +1,10 @@
import React, { useState, memo } from 'react';
import React, { useEffect, useState, memo } from 'react';
import Modal from './ui/modal/Modal';
import './FolderModal.css';
import type { FolderData } from '../lib/4nk/models/FolderData';
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
interface FolderModalProps {
folder?: FolderData;
onSave?: (folderData: FolderData) => void;
@ -10,6 +12,11 @@ interface FolderModalProps {
readOnly?: boolean;
isOpen: boolean;
onClose: () => void;
folderType?: FolderType;
renderExtraFields?: (
folderData: FolderData,
setFolderData: React.Dispatch<React.SetStateAction<FolderData>>
) => React.ReactNode;
}
const defaultFolder: FolderData = {
@ -27,80 +34,92 @@ const defaultFolder: FolderData = {
stakeholders: []
};
function capitalize(s?: string) {
if (!s) return '';
return s.charAt(0).toUpperCase() + s.slice(1);
}
function FolderModal({
folder = defaultFolder,
onSave,
onCancel,
readOnly = false,
isOpen,
onClose
onClose,
folderType = 'autre',
renderExtraFields
}: FolderModalProps) {
const [folderData, setFolderData] = useState<FolderData>(folder);
const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder });
const [currentCustomer, setCurrentCustomer] = useState<string>('');
const [currentStakeholder, setCurrentStakeholder] = useState<string>('');
const [currentNote, setCurrentNote] = useState<string>('');
if (!isOpen) return null;
// Sync when modal opens or when folder prop changes (useful pour Edit)
useEffect(() => {
if (isOpen) {
// Merge with defaultFolder to ensure arrays exist
setFolderData({ ...defaultFolder, ...(folder || {}) });
setCurrentCustomer('');
setCurrentStakeholder('');
setCurrentNote('');
}
}, [isOpen, folder]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
// Generic input change handler
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFolderData(prev => ({
...prev,
[name]: value
}));
// cast to avoid TS complaints when updating dynamic fields
setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData));
};
/* ---------- Customers ---------- */
const addCustomer = () => {
if (currentCustomer.trim() && !folderData.customers.includes(currentCustomer.trim())) {
setFolderData(prev => ({
...prev,
customers: [...prev.customers, currentCustomer.trim()]
}));
setCurrentCustomer('');
const v = currentCustomer.trim();
if (!v) return;
if (!Array.isArray(folderData.customers)) folderData.customers = [];
if (!folderData.customers.includes(v)) {
setFolderData(prev => ({ ...prev, customers: [...(prev.customers || []), v] }));
}
setCurrentCustomer('');
};
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 = () => {
if (currentStakeholder.trim() && !folderData.stakeholders.includes(currentStakeholder.trim())) {
setFolderData(prev => ({
...prev,
stakeholders: [...prev.stakeholders, currentStakeholder.trim()]
}));
setCurrentStakeholder('');
const v = currentStakeholder.trim();
if (!v) return;
if (!Array.isArray(folderData.stakeholders)) folderData.stakeholders = [];
if (!folderData.stakeholders.includes(v)) {
setFolderData(prev => ({ ...prev, stakeholders: [...(prev.stakeholders || []), v] }));
}
setCurrentStakeholder('');
};
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 = () => {
if (currentNote.trim() && !folderData.notes.includes(currentNote.trim())) {
setFolderData(prev => ({
...prev,
notes: [...prev.notes, currentNote.trim()]
}));
setCurrentNote('');
const v = currentNote.trim();
if (!v) return;
if (!Array.isArray(folderData.notes)) folderData.notes = [];
if (!folderData.notes.includes(v)) {
setFolderData(prev => ({ ...prev, notes: [...(prev.notes || []), v] }));
}
setCurrentNote('');
};
const removeNote = (note: string) => {
setFolderData(prev => ({
...prev,
notes: prev.notes.filter(m => m !== note)
}));
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) }));
};
/* ---------- Submit / Cancel ---------- */
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSave) {
@ -109,22 +128,32 @@ function FolderModal({
updated_at: new Date().toISOString()
});
}
if (onClose) {
onClose(); // ← ça ferme le modal après sauvegarde
}
};
const handleCancel = () => {
if (onCancel) {
onCancel();
} else {
onClose();
onCancel(); // ton callback spécifique
} else if (onClose) {
onClose(); // fallback si pas de onCancel
}
};
// Title text
const title = `Créer un dossier ${capitalize(folderType)}`;
return (
<Modal isOpen={isOpen} onClose={onClose} title="Créer un nouveau dossier" size="lg">
<div className="folder-container">
// On ne fait PAS "if (!isOpen) return null" : Modal gère l'animation/visibilité
<Modal isOpen={isOpen} onClose={onClose} title={title} size="lg">
<div className={`folder-container folder-${folderType}`}>
<form className="folder-form" onSubmit={handleSubmit}>
{/* Informations principales */}
<div className="form-section">
<h3 className="section-title">Informations principales</h3>
<div className="form-row">
<div className="form-field">
<label>
@ -133,13 +162,14 @@ function FolderModal({
<input
type="text"
name="folderNumber"
value={folderData.folderNumber}
value={folderData.folderNumber || ''}
onChange={handleInputChange}
required
disabled={readOnly}
placeholder="ex: DOC-2025-001"
/>
</div>
<div className="form-field">
<label>
Nom <span className="required">*</span>
@ -147,7 +177,7 @@ function FolderModal({
<input
type="text"
name="name"
value={folderData.name}
value={folderData.name || ''}
onChange={handleInputChange}
required
disabled={readOnly}
@ -156,52 +186,11 @@ function FolderModal({
</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">
<label>Description</label>
<textarea
name="description"
value={folderData.description}
value={folderData.description || ''}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Description du dossier"
@ -214,7 +203,7 @@ function FolderModal({
<label>Description d'archivage</label>
<textarea
name="archived_description"
value={folderData.archived_description}
value={folderData.archived_description || ''}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Raison d'archivage"
@ -224,100 +213,19 @@ function FolderModal({
)}
</div>
<div className="form-section">
<h3 className="section-title">Clients</h3>
<div className="tag-list">
{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>
))}
{/* Champs spécifiques injectés */}
{renderExtraFields && (
<div className="form-section">
{renderExtraFields(folderData, setFolderData)}
</div>
)}
{!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>
{/* Notes */}
<div className="form-section">
<h3 className="section-title">Notes</h3>
<div className="tag-list">
{folderData.notes.map((note, index) => (
{(folderData.notes || []).map((note, index) => (
<div key={index} className="tag-item">
<span>{note}</span>
{!readOnly && (
@ -341,7 +249,7 @@ function FolderModal({
value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)}
placeholder="Ajouter une note"
onKeyPress={(e) => {
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNote();
@ -359,14 +267,16 @@ function FolderModal({
)}
</div>
{/* Informations système */}
<div className="form-section">
<h3 className="section-title">Informations système</h3>
<div className="form-row">
<div className="form-field">
<label>Créé le</label>
<input
type="text"
value={new Date(folderData.created_at).toLocaleDateString('fr-FR', {
value={new Date(folderData.created_at).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
@ -377,11 +287,12 @@ function FolderModal({
readOnly
/>
</div>
<div className="form-field">
<label>Dernière mise à jour</label>
<input
type="text"
value={new Date(folderData.updated_at).toLocaleDateString('fr-FR', {
value={new Date(folderData.updated_at).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
@ -395,19 +306,12 @@ function FolderModal({
</div>
</div>
{/* Actions */}
<div className="form-actions">
<button
type="button"
className="btn-cancel"
onClick={handleCancel}
>
<button type="button" className="btn-cancel" onClick={handleCancel}>
Annuler
</button>
<button
type="submit"
className="btn-submit"
disabled={readOnly}
>
<button type="submit" className="btn-submit" disabled={readOnly}>
Enregistrer
</button>
</div>
@ -415,7 +319,7 @@ function FolderModal({
</div>
</Modal>
);
};
}
FolderModal.displayName = 'FolderModal';
export default memo(FolderModal);

View File

@ -0,0 +1,325 @@
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);

View File

@ -1,163 +0,0 @@
.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

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

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { X } from 'lucide-react';
import './Modal.css';
@ -10,14 +10,26 @@ interface ModalProps {
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'md'
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'md'
}) => {
if (!isOpen) return null;
const [isVisible, setIsVisible] = useState(isOpen);
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) => {
if (e.target === e.currentTarget) {
@ -26,8 +38,8 @@ const Modal: React.FC<ModalProps> = ({
};
return (
<div className="modal-overlay" onClick={handleBackdropClick}>
<div className={`modal-container modal-${size}`}>
<div className={`modal-overlay ${isOpen ? 'fade-in' : 'fade-out'}`} onClick={handleBackdropClick}>
<div className={`modal-container modal-${size} ${isOpen ? 'slide-in' : 'slide-out'}`}>
<div className="modal-header">
<h2 className="modal-title">{title}</h2>
<button
@ -38,9 +50,7 @@ const Modal: React.FC<ModalProps> = ({
<X className="h-5 w-5" />
</button>
</div>
<div className="modal-content">
{children}
</div>
<div className="modal-content">{children}</div>
</div>
</div>
);

View File

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

135
lib/4nk/models/RhData.ts Normal file
View File

@ -0,0 +1,135 @@
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: []
}
}
};