Compare commits
9 Commits
8ee7b8a177
...
c4edfbd6de
| Author | SHA1 | Date | |
|---|---|---|---|
| c4edfbd6de | |||
| d88697f282 | |||
| 741ad90b00 | |||
| e625431d26 | |||
| b32fccdb7f | |||
| df1e345748 | |||
| 73d1d47cd1 | |||
| 6d9118b564 | |||
| dabe614fde |
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
36
app/page.tsx
36
app/page.tsx
@ -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 d’authentification */}
|
||||
{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">
|
||||
|
||||
@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
325
components/ProcessesViewer.tsx
Normal file
325
components/ProcessesViewer.tsx
Normal 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);
|
||||
@ -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%;
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
135
lib/4nk/models/RhData.ts
Normal 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: []
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user