Fix some code

This commit is contained in:
Anthony Janin 2025-06-05 10:04:10 +02:00
parent 2c329aa8a2
commit 1e6065ec7c
10 changed files with 323 additions and 334 deletions

View File

@ -1,54 +1,50 @@
# React + TypeScript + Vite # Application [4NK] - Interface Web
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Cette application React fournit une interface pour interagir avec la plateforme [4NK]. Elle permet l'authentification des utilisateurs, la création de profils et de dossiers, ainsi que la visualisation des processus.
Currently, two official plugins are available: ## Fonctionnalités principales
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh - Authentification utilisateur via OAuth
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - Création et gestion de profils utilisateur
- Création et gestion de dossiers
- Visualisation des processus
- Console de messages pour le suivi des événements
## Expanding the ESLint configuration ## Prérequis
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - Node.js (version 18 ou supérieure)
- npm ou yarn
```js ## Installation
export default tseslint.config({
extends: [ Pour installer les dépendances du projet, exécutez :
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked, ```bash
// Alternatively, use this for stricter rules npm install
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ## Commandes disponibles
```js ### Démarrage de l'application
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({ **Commande principale** pour lancer l'application en mode développement :
plugins: {
// Add the react-x and react-dom plugins ```bash
'react-x': reactX, npm run dev
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
``` ```
L'application sera accessible à l'adresse : http://localhost:5173
## Communication avec la plateforme [4NK]
L'application communique avec la plateforme [4NK] via une iframe et un bus de messages. La communication est gérée par les classes `MessageBus` et `EventBus` dans le dossier `/src/sdk`.
## Configuration
L'URL de l'iframe est définie dans `App.tsx` :
```typescript
const iframeUrl = 'https://dev3.4nkweb.com'
```
Pour modifier l'environnement cible, vous devez changer cette URL.

View File

@ -147,6 +147,7 @@ function App() {
onLogout={handleLogout} onLogout={handleLogout}
onCreateProfile={handleOpenProfileModal} onCreateProfile={handleOpenProfileModal}
onCreateFolder={handleOpenFolderModal} onCreateFolder={handleOpenFolderModal}
iframeUrl={iframeUrl}
/> />
{/* Structure flexible avec console à gauche et contenu à droite */} {/* Structure flexible avec console à gauche et contenu à droite */}

View File

@ -9,28 +9,22 @@ interface ControlPanelProps {
iframeUrl?: string; iframeUrl?: string;
} }
/** function ControlPanel({
* Composant de panel de contrôle pour gérer les actions principales
*
* Fournit des boutons pour la connexion, la création de profil et le contrôle de visibilité
*/
function ControlPanel({
onLogin = () => console.log('Connexion demandée'), onLogin = () => console.log('Connexion demandée'),
onLogout = () => console.log('Déconnexion demandée'), onLogout = () => console.log('Déconnexion demandée'),
onCreateProfile = () => console.log('Création de profil demandée'), onCreateProfile = () => console.log('Création de profil demandée'),
onCreateFolder = () => console.log('Création de dossier demandée'), onCreateFolder = () => console.log('Création de dossier demandée'),
isConnected = false, isConnected = false,
iframeUrl = 'https://dev1.4nkweb.com' iframeUrl = ''
}: ControlPanelProps) { }: ControlPanelProps) {
// État pour gérer l'affichage du tooltip
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
return ( return (
<div className="control-panel-container"> <div className="control-panel-container">
<div className="control-panel-row"> <div className="control-panel-row">
{/* Bouton d'information avec tooltip */} {/* Bouton d'information avec tooltip */}
<div className="url-tooltip-container button-container"> <div className="url-tooltip-container button-container">
<button <button
className="info-button" className="info-button"
onMouseEnter={() => setShowTooltip(true)} onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)} onMouseLeave={() => setShowTooltip(false)}
@ -44,19 +38,19 @@ function ControlPanel({
</div> </div>
)} )}
</div> </div>
{/* Boutons de contrôle */} {/* Boutons de contrôle */}
<div className="button-container"> <div className="button-container">
<button <button
className={`control-button ${isConnected ? 'logout-button' : ''}`} className={`control-button ${isConnected ? 'logout-button' : ''}`}
onClick={isConnected ? onLogout : onLogin} onClick={isConnected ? onLogout : onLogin}
> >
{isConnected ? 'Se déconnecter' : 'Se connecter'} {isConnected ? 'Se déconnecter' : 'Se connecter'}
</button> </button>
</div> </div>
<div className="button-container"> <div className="button-container">
<button <button
className={`control-button ${!(isConnected) ? 'disabled' : ''}`} className={`control-button ${!(isConnected) ? 'disabled' : ''}`}
onClick={onCreateProfile} onClick={onCreateProfile}
disabled={!(isConnected)} disabled={!(isConnected)}
> >
@ -64,8 +58,8 @@ function ControlPanel({
</button> </button>
</div> </div>
<div className="button-container"> <div className="button-container">
<button <button
className={`control-button ${!(isConnected) ? 'disabled' : ''}`} className={`control-button ${!(isConnected) ? 'disabled' : ''}`}
onClick={onCreateFolder} onClick={onCreateFolder}
disabled={!(isConnected)} disabled={!(isConnected)}
> >

View File

@ -77,7 +77,7 @@
font-style: italic; font-style: italic;
} }
.form-field input, .form-field input,
.form-field select, .form-field select,
.form-field textarea { .form-field textarea {
background-color: rgba(255, 255, 255, 0.08); background-color: rgba(255, 255, 255, 0.08);
@ -264,6 +264,7 @@
0% { 0% {
box-shadow: 0 0 0 2px rgba(103, 58, 183, 0.8); box-shadow: 0 0 0 2px rgba(103, 58, 183, 0.8);
} }
100% { 100% {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
@ -279,16 +280,13 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 0.5rem; gap: 0.5rem;
} }
.form-actions { .form-actions {
flex-direction: column; flex-direction: column;
} }
.btn-cancel, .btn-submit { .btn-cancel,
.btn-submit {
width: 100%; width: 100%;
} }
} }
/* Styles spécifiques pour le composant FolderModal */
/* Les styles génériques de modal sont fournis par Modal.css */

View File

@ -27,10 +27,10 @@ const defaultFolder: FolderData = {
stakeholders: [] stakeholders: []
}; };
function FolderModal({ function FolderModal({
folder = defaultFolder, folder = defaultFolder,
onSave, onSave,
onCancel, onCancel,
readOnly = false, readOnly = false,
isOpen, isOpen,
onClose onClose
@ -96,239 +96,239 @@ function FolderModal({
return ( return (
<Modal isOpen={isOpen} onClose={onClose} title="Créer un nouveau dossier"> <Modal isOpen={isOpen} onClose={onClose} title="Créer un nouveau dossier">
<div className="folder-container"> <div className="folder-container">
<form className="folder-form" onSubmit={handleSubmit}> <form className="folder-form" onSubmit={handleSubmit}>
<div className="form-section"> <div className="form-section">
<h3 className="section-title">Informations principales</h3> <h3 className="section-title">Informations principales</h3>
<div className="form-row"> <div className="form-row">
<div className="form-field"> <div className="form-field">
<label> <label>
Numéro de dossier <span className="required">*</span> Numéro de dossier <span className="required">*</span>
</label> </label>
<input <input
type="text" type="text"
name="folderNumber" name="folderNumber"
value={folderData.folderNumber} value={folderData.folderNumber}
onChange={handleInputChange} onChange={handleInputChange}
required required
disabled={readOnly} disabled={readOnly}
placeholder="ex: DOC-2025-001" placeholder="ex: DOC-2025-001"
/> />
</div>
<div className="form-field">
<label>
Nom <span className="required">*</span>
</label>
<input
type="text"
name="name"
value={folderData.name}
onChange={handleInputChange}
required
disabled={readOnly}
placeholder="Nom du dossier"
/>
</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="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}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Description du dossier"
rows={3}
/>
</div>
{folderData.status === 'archived' && (
<div className="form-field">
<label>Description d'archivage</label>
<textarea
name="archived_description"
value={folderData.archived_description}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Raison d'archivage"
rows={2}
/>
</div>
)}
</div> </div>
<div className="form-field">
<div className="form-section"> <label>
<h3 className="section-title">Clients</h3> Nom <span className="required">*</span>
<div className="tag-list"> </label>
{folderData.customers.map((customer, index) => ( <input
<div key={index} className="tag-item"> type="text"
<span>{customer}</span> name="name"
{!readOnly && ( value={folderData.name}
<button onChange={handleInputChange}
type="button" required
className="tag-remove" disabled={readOnly}
onClick={() => removeCustomer(customer)} placeholder="Nom du dossier"
aria-label="Supprimer ce client" />
>
×
</button>
)}
</div>
))}
</div>
{!readOnly && (
<div className="form-field tag-input-container">
<input
type="text"
value={currentCustomer}
onChange={(e) => setCurrentCustomer(e.target.value)}
placeholder="Ajouter un client"
/>
<button
type="button"
className="btn-add-tag"
onClick={addCustomer}
>
Ajouter
</button>
</div>
)}
</div> </div>
</div>
<div className="form-section"> <div className="form-row">
<h3 className="section-title">Parties prenantes</h3> <div className="form-field">
<div className="tag-list"> <label>
{folderData.stakeholders.map((stakeholder, index) => ( Type d'acte <span className="required">*</span>
<div key={index} className="tag-item"> </label>
<span>{stakeholder}</span> <select
{!readOnly && ( name="deedType"
<button value={folderData.deedType}
type="button" onChange={handleInputChange}
className="tag-remove" required
onClick={() => removeStakeholder(stakeholder)} disabled={readOnly}
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"
/>
<button
type="button"
className="btn-add-tag"
onClick={addStakeholder}
>
Ajouter
</button>
</div>
)}
</div>
<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', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
disabled
readOnly
/>
</div>
<div className="form-field">
<label>Dernière mise à jour</label>
<input
type="text"
value={new Date(folderData.updated_at).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
disabled
readOnly
/>
</div>
</div>
</div>
<div className="form-actions">
{onCancel && (
<button
type="button"
className="btn-cancel"
onClick={onCancel}
>
Annuler
</button>
)}
<button
type="submit"
className="btn-submit"
> >
Enregistrer <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="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}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Description du dossier"
rows={3}
/>
</div>
{folderData.status === 'archived' && (
<div className="form-field">
<label>Description d'archivage</label>
<textarea
name="archived_description"
value={folderData.archived_description}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Raison d'archivage"
rows={2}
/>
</div>
)}
</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>
))}
</div>
{!readOnly && (
<div className="form-field tag-input-container">
<input
type="text"
value={currentCustomer}
onChange={(e) => setCurrentCustomer(e.target.value)}
placeholder="Ajouter un client"
/>
<button
type="button"
className="btn-add-tag"
onClick={addCustomer}
>
Ajouter
</button> </button>
</div> </div>
</form> )}
</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"
/>
<button
type="button"
className="btn-add-tag"
onClick={addStakeholder}
>
Ajouter
</button>
</div>
)}
</div>
<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', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
disabled
readOnly
/>
</div>
<div className="form-field">
<label>Dernière mise à jour</label>
<input
type="text"
value={new Date(folderData.updated_at).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
disabled
readOnly
/>
</div>
</div>
</div>
<div className="form-actions">
{onCancel && (
<button
type="button"
className="btn-cancel"
onClick={onCancel}
>
Annuler
</button>
)}
<button
type="submit"
className="btn-submit"
>
Enregistrer
</button>
</div>
</form>
</div>
</Modal> </Modal>
); );
}; };

View File

@ -7,18 +7,18 @@ import EventBus from '../sdk/EventBus';
const formatTimestamp = (isoString: string): string => { const formatTimestamp = (isoString: string): string => {
const date = new Date(isoString); const date = new Date(isoString);
const now = new Date(); const now = new Date();
// Obtenir les composants de l'heure // Obtenir les composants de l'heure
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0');
const ms = String(date.getMilliseconds()).padStart(3, '0'); const ms = String(date.getMilliseconds()).padStart(3, '0');
// Vérifier si c'est aujourd'hui // Vérifier si c'est aujourd'hui
const isToday = date.getDate() === now.getDate() && const isToday = date.getDate() === now.getDate() &&
date.getMonth() === now.getMonth() && date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear(); date.getFullYear() === now.getFullYear();
if (isToday) { if (isToday) {
// Format pour aujourd'hui: HH:MM:SS.ms // Format pour aujourd'hui: HH:MM:SS.ms
return `${hours}:${minutes}:${seconds}.${ms}`; return `${hours}:${minutes}:${seconds}.${ms}`;
@ -31,7 +31,7 @@ const formatTimestamp = (isoString: string): string => {
}; };
interface MessageConsoleProps { interface MessageConsoleProps {
messages: {timestamp: string; data: any}[]; messages: { timestamp: string; data: any }[];
onClearMessages?: () => void; onClearMessages?: () => void;
} }
@ -41,18 +41,18 @@ interface MessageConsoleProps {
* Affiche une liste de messages horodatés dans un panneau de console * Affiche une liste de messages horodatés dans un panneau de console
* S'abonne automatiquement aux messages du EventBus * S'abonne automatiquement aux messages du EventBus
*/ */
function MessageConsole({ function MessageConsole({
messages: externalMessages = [], messages: externalMessages = [],
onClearMessages = () => console.log('Vidage des messages demandé') onClearMessages = () => console.log('Vidage des messages demandé')
}: MessageConsoleProps) { }: MessageConsoleProps) {
// État local pour stocker les messages reçus via l'EventBus // État local pour stocker les messages reçus via l'EventBus
const [localMessages, setLocalMessages] = useState<{timestamp: string; data: any}[]>([]); const [localMessages, setLocalMessages] = useState<{ timestamp: string; data: any }[]>([]);
// Tous les messages à afficher (externes + locaux), triés par timestamp // Tous les messages à afficher (externes + locaux), triés par timestamp
const allMessages = [...externalMessages, ...localMessages].sort((a, b) => { const allMessages = [...externalMessages, ...localMessages].sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
}); });
// Gestionnaire de message reçu via EventBus // Gestionnaire de message reçu via EventBus
const handleMessageReceived = (message: any) => { const handleMessageReceived = (message: any) => {
const newMessage = { const newMessage = {
@ -65,19 +65,19 @@ function MessageConsole({
// S'abonner aux événements // S'abonner aux événements
useEffect(() => { useEffect(() => {
const unsubscribeMessageReceived = EventBus.getInstance().on('MESSAGE_RECEIVED', handleMessageReceived); const unsubscribeMessageReceived = EventBus.getInstance().on('MESSAGE_RECEIVED', handleMessageReceived);
// S'abonner à l'événement CLEAR_CONSOLE pour vider les messages locaux lors de la déconnexion // S'abonner à l'événement CLEAR_CONSOLE pour vider les messages locaux lors de la déconnexion
const unsubscribeClearConsole = EventBus.getInstance().on('CLEAR_CONSOLE', () => { const unsubscribeClearConsole = EventBus.getInstance().on('CLEAR_CONSOLE', () => {
setLocalMessages([]); setLocalMessages([]);
}); });
// Se désabonner lors du nettoyage // Se désabonner lors du nettoyage
return () => { return () => {
unsubscribeMessageReceived(); unsubscribeMessageReceived();
unsubscribeClearConsole(); unsubscribeClearConsole();
}; };
}, []); }, []);
// Fonction pour vider les messages locaux et externes // Fonction pour vider les messages locaux et externes
const handleClearAll = () => { const handleClearAll = () => {
setLocalMessages([]); setLocalMessages([]);
@ -85,7 +85,7 @@ function MessageConsole({
}; };
// Référence pour le conteneur de la console de messages // Référence pour le conteneur de la console de messages
const consoleContentRef = useRef<HTMLDivElement>(null); const consoleContentRef = useRef<HTMLDivElement>(null);
// Effet pour faire défiler automatiquement vers le bas quand de nouveaux messages arrivent // Effet pour faire défiler automatiquement vers le bas quand de nouveaux messages arrivent
useEffect(() => { useEffect(() => {
if (consoleContentRef.current && allMessages.length > 0) { if (consoleContentRef.current && allMessages.length > 0) {
@ -100,8 +100,8 @@ function MessageConsole({
<h3>Console</h3> <h3>Console</h3>
<div className="console-controls"> <div className="console-controls">
<span className="message-count">{allMessages.length} message(s)</span> <span className="message-count">{allMessages.length} message(s)</span>
<button <button
className="clear-console-button" className="clear-console-button"
onClick={handleClearAll} onClick={handleClearAll}
disabled={allMessages.length === 0} disabled={allMessages.length === 0}
title="Vider les messages" title="Vider les messages"
@ -119,8 +119,8 @@ function MessageConsole({
<li key={index} className="message-item"> <li key={index} className="message-item">
<div className="message-timestamp">{formatTimestamp(message.timestamp)}</div> <div className="message-timestamp">{formatTimestamp(message.timestamp)}</div>
<pre className="message-data"> <pre className="message-data">
{typeof message.data === 'object' {typeof message.data === 'object'
? JSON.stringify(message.data, null, 2) ? JSON.stringify(message.data, null, 2)
: String(message.data)} : String(message.data)}
</pre> </pre>
</li> </li>

View File

@ -171,4 +171,4 @@
word-break: break-all; word-break: break-all;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
font-size: 0.8rem; font-size: 0.8rem;
} }

View File

@ -37,16 +37,16 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
} }
const toggleBlock = (blockId: string) => { const toggleBlock = (blockId: string) => {
setExpandedBlocks(prev => setExpandedBlocks(prev =>
prev.includes(blockId) prev.includes(blockId)
? prev.filter(id => id !== blockId) ? prev.filter(id => id !== blockId)
: [...prev, blockId] : [...prev, blockId]
); );
}; };
const formatAddress = (address: string | number[] | undefined): string => { const formatAddress = (address: string | number[] | undefined): string => {
if (!address) return "Adresse non disponible"; if (!address) return "Adresse non disponible";
if (Array.isArray(address)) { if (Array.isArray(address)) {
// Si c'est un tableau de nombres, on le convertit en chaîne de caractères // Si c'est un tableau de nombres, on le convertit en chaîne de caractères
try { try {
@ -66,7 +66,7 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
const formatName = (name: string | number[] | undefined): string => { const formatName = (name: string | number[] | undefined): string => {
if (!name) return "Nom non disponible"; if (!name) return "Nom non disponible";
if (Array.isArray(name)) { if (Array.isArray(name)) {
if (name.length === 1 && name[0] === 96) { if (name.length === 1 && name[0] === 96) {
return "`"; // Caractère spécial return "`"; // Caractère spécial
@ -88,30 +88,30 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
<div className="processes-viewer"> <div className="processes-viewer">
<h2>Processus</h2> <h2>Processus</h2>
<p className="block-count">{Object.keys(processes).length} processus disponible(s)</p> <p className="block-count">{Object.keys(processes).length} processus disponible(s)</p>
<div className="block-list"> <div className="block-list">
{Object.entries(processes).map(([blockId, block]) => { {Object.entries(processes).map(([blockId, block]) => {
const isExpanded = expandedBlocks.includes(blockId); const isExpanded = expandedBlocks.includes(blockId);
const stateCount = block.states.length; const stateCount = block.states.length;
// Le premier état est le plus récent // Le premier état est le plus récent
return ( return (
<div key={blockId} className="block-item"> <div key={blockId} className="block-item">
<div <div
className={`block-header ${isExpanded ? 'expanded' : ''}`} className={`block-header ${isExpanded ? 'expanded' : ''}`}
onClick={() => toggleBlock(blockId)} onClick={() => toggleBlock(blockId)}
> >
<div className="block-id">{blockId.substring(0, 8)}...{blockId.substring(blockId.length - 4)}</div> <div className="block-id">{blockId.substring(0, 8)}...{blockId.substring(blockId.length - 4)}</div>
<div className="block-state-count">{stateCount} état(s)</div> <div className="block-state-count">{stateCount} état(s)</div>
<div className="block-toggle">{isExpanded ? '▼' : '▶'}</div> <div className="block-toggle">{isExpanded ? '▼' : '▶'}</div>
</div> </div>
{isExpanded && ( {isExpanded && (
<div className="block-details"> <div className="block-details">
<div className="block-complete-id"> <div className="block-complete-id">
<strong>ID complet:</strong> {blockId} <strong>ID complet:</strong> {blockId}
</div> </div>
{block.states.map((state, index) => ( {block.states.map((state, index) => (
<div key={`${blockId}-state-${index}`} className="state-item"> <div key={`${blockId}-state-${index}`} className="state-item">
<h4>État {index + 1}</h4> <h4>État {index + 1}</h4>
@ -121,7 +121,7 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
<div className="state-detail"> <div className="state-detail">
<strong>Commited dans:</strong> {state.commited_in} <strong>Commited dans:</strong> {state.commited_in}
</div> </div>
<div className="state-public-data"> <div className="state-public-data">
<h5>Données publiques</h5> <h5>Données publiques</h5>
<div className="public-data-item"> <div className="public-data-item">
@ -131,7 +131,7 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
<div className="public-data-item"> <div className="public-data-item">
<strong>Adresses associées:</strong> <strong>Adresses associées:</strong>
<ul className="address-list"> <ul className="address-list">
{Array.isArray(state.public_data.pairedAddresses) ? {Array.isArray(state.public_data.pairedAddresses) ?
(typeof state.public_data.pairedAddresses[0] === 'string' ? ( (typeof state.public_data.pairedAddresses[0] === 'string' ? (
(state.public_data.pairedAddresses as string[]).map((addr, i) => ( (state.public_data.pairedAddresses as string[]).map((addr, i) => (
<li key={i}>{addr}</li> <li key={i}>{addr}</li>

View File

@ -171,4 +171,4 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 0.5rem; gap: 0.5rem;
} }
} }

View File

@ -2,7 +2,7 @@ export default class EventBus {
private static instance: EventBus; private static instance: EventBus;
private listeners: Record<string, Array<(...args: any[]) => void>> = {}; private listeners: Record<string, Array<(...args: any[]) => void>> = {};
private constructor() {} private constructor() { }
public static getInstance(): EventBus { public static getInstance(): EventBus {
if (!EventBus.instance) { if (!EventBus.instance) {