@osfarm/itineraire-technique 1.1.19 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,10 +24,42 @@ Le visualisateur est fourni avec un éditeur qui permet de créer son propre iti
24
24
  Ce visualisateur est avant tout conçu pour être utilisé sur [Triple Performance](https://wiki.tripleperformance.fr/). Vous y trouverez de [nombreux](https://wiki.tripleperformance.fr/wiki/Retours_d%27exp%C3%A9rience) [retours d'expérience](https://wiki.tripleperformance.fr/wiki/Ferme_de_Longueil) documentés avec des données technico-économiques ainsi que les itinéraires techniques associés. Les itinéraires peuvent être créés alors directement dans [Google Spreadsheet](https://wiki.tripleperformance.fr/wiki/Aide:Ins%C3%A9rer_des_graphiques_dans_une_page) grâce à l'[add-on](https://workspace.google.com/marketplace/app/triple_performance/427792115089) spécifiquement conçu pour Google Workspace.
25
25
 
26
26
  ## Utilisation dans un autre contexte / logiciel
27
+
27
28
  Il est possible d'utiliser cette librairie très facilement dans n'importe quel outil. Le visualisateur a été conçu pour être très facile à intégrer dans une page HTML, il ne dépend que de briques Javascript (Apache Echarts, JQuery et Bootstrap). N'hésitez pas à nous contacter si vous décidez de l'utiliser et à contribuer si vous faites des évolutions !
28
-
29
29
 
30
- Pour utiliser le package, le plus simple est d'utiliser npm :
30
+ ### 🆕 Composants React/Next.js
31
+
32
+ **Nouveauté version 1.2.0** : Le projet inclut désormais des composants React/Next.js prêts à l'emploi !
33
+
34
+ ```bash
35
+ npm i @osfarm/itineraire-technique
36
+ ```
37
+
38
+ **Utilisation rapide avec React/Next.js :**
39
+
40
+ ```tsx
41
+ import { TikaRenderer } from '@osfarm/itineraire-technique/react';
42
+
43
+ function MyComponent() {
44
+ const data = { /* vos données JSON */ };
45
+ return <TikaRenderer data={data} />;
46
+ }
47
+ ```
48
+
49
+ **📚 [Documentation complète React/Next.js](react/README.md)**
50
+
51
+ Consultez le guide complet avec :
52
+ - Composants `TikaRenderer` et `TikaEditor`
53
+ - Hooks personnalisés (`useItineraire`, etc.)
54
+ - Types TypeScript
55
+ - Exemples Next.js App Router et Pages Router
56
+ - Configuration et intégration
57
+
58
+ **🔗 [Exemples d'intégration](examples/)**
59
+
60
+ ### Utilisation vanilla JS/HTML
61
+
62
+ Pour utiliser le package en JavaScript vanilla, le plus simple est d'utiliser npm :
31
63
 
32
64
  ```
33
65
  npm i @osfarm/itineraire-technique
@@ -0,0 +1,137 @@
1
+ # Exemples d'utilisation
2
+
3
+ Ce dossier contient des exemples d'intégration du visualisateur d'itinéraires techniques TIKA dans différents contextes.
4
+
5
+ ## Fichiers disponibles
6
+
7
+ ### Next.js
8
+
9
+ - **`nextjs-app-router-viewer.tsx`** - Exemple de page de visualisation avec Next.js App Router (13+)
10
+ - Affichage d'un itinéraire technique
11
+ - Gestion du chargement des données
12
+ - Événements de clic et survol
13
+
14
+ - **`nextjs-app-router-editor.tsx`** - Exemple d'éditeur complet avec Next.js App Router
15
+ - Interface complète d'édition
16
+ - Ajout/modification/suppression d'étapes
17
+ - Gestion des interventions
18
+ - Sauvegarde et export
19
+
20
+ - **`nextjs-_document.tsx`** - Configuration du document Next.js
21
+ - Chargement des dépendances CDN
22
+ - Configuration des scripts et styles
23
+ - Compatible Pages Router
24
+
25
+ - **`nextjs-api-route.ts`** - Exemple d'API Routes
26
+ - Sauvegarde et chargement d'itinéraires
27
+ - CRUD complet
28
+ - Gestion des fichiers JSON
29
+
30
+ ## Utilisation
31
+
32
+ ### 1. Copier les exemples
33
+
34
+ Copiez les fichiers dans votre projet Next.js :
35
+
36
+ ```bash
37
+ # Pour le visualiseur
38
+ cp examples/nextjs-app-router-viewer.tsx app/itineraire/page.tsx
39
+
40
+ # Pour l'éditeur
41
+ cp examples/nextjs-app-router-editor.tsx app/editor/page.tsx
42
+
43
+ # Pour la configuration
44
+ cp examples/nextjs-_document.tsx pages/_document.tsx # Pages Router
45
+ # ou adaptez pour app/layout.tsx (App Router)
46
+
47
+ # Pour l'API
48
+ cp examples/nextjs-api-route.ts app/api/itineraire/route.ts # App Router
49
+ # ou pages/api/itineraire.ts (Pages Router)
50
+ ```
51
+
52
+ ### 2. Installer les dépendances
53
+
54
+ ```bash
55
+ npm install @osfarm/itineraire-technique
56
+ npm run setup:react
57
+ ```
58
+
59
+ ### 3. Adapter à votre projet
60
+
61
+ Les exemples sont commentés et peuvent être adaptés à vos besoins spécifiques :
62
+
63
+ - Personnalisez les styles
64
+ - Ajoutez votre logique métier
65
+ - Intégrez avec votre base de données
66
+ - Ajoutez l'authentification
67
+
68
+ ## Structure d'un projet Next.js complet
69
+
70
+ ```
71
+ my-app/
72
+ ├── app/ # App Router (Next.js 13+)
73
+ │ ├── layout.tsx # Layout principal avec scripts CDN
74
+ │ ├── itineraire/
75
+ │ │ └── page.tsx # Page de visualisation
76
+ │ ├── editor/
77
+ │ │ └── page.tsx # Page d'édition
78
+ │ └── api/
79
+ │ └── itineraire/
80
+ │ └── route.ts # API Routes
81
+ ├── public/ # Fichiers statiques
82
+ │ ├── js/
83
+ │ │ ├── chart-render.js
84
+ │ │ └── editor-*.js
85
+ │ ├── css/
86
+ │ │ ├── styles-rendering.css
87
+ │ │ └── styles-editor.css
88
+ │ └── test/
89
+ │ └── test.json
90
+ ├── package.json
91
+ └── tsconfig.json
92
+ ```
93
+
94
+ ## Frameworks alternatifs
95
+
96
+ Bien que ces exemples soient pour Next.js, les composants React sont compatibles avec :
97
+
98
+ - **Create React App** - Ajoutez les scripts CDN dans `public/index.html`
99
+ - **Vite** - Ajoutez les scripts CDN dans `index.html`
100
+ - **Remix** - Ajoutez les scripts dans `root.tsx`
101
+ - **Gatsby** - Utilisez `gatsby-ssr.js` pour ajouter les scripts
102
+
103
+ ## Support TypeScript
104
+
105
+ Tous les exemples sont fournis en TypeScript avec les types complets. Si vous utilisez JavaScript, supprimez simplement les annotations de type.
106
+
107
+ ## Personnalisation
108
+
109
+ ### Thème et styles
110
+
111
+ Vous pouvez personnaliser les styles en :
112
+
113
+ 1. Surchargeant les classes CSS existantes
114
+ 2. Modifiant les fichiers SCSS sources
115
+ 3. Utilisant la prop `className` des composants
116
+
117
+ ### Événements
118
+
119
+ Les composants exposent des callbacks pour interagir avec eux :
120
+
121
+ ```tsx
122
+ <TikaRenderer
123
+ data={data}
124
+ onItemClick={(id, event) => {
125
+ // Votre logique
126
+ }}
127
+ onItemHover={(id, event) => {
128
+ // Votre logique
129
+ }}
130
+ />
131
+ ```
132
+
133
+ ## Besoin d'aide ?
134
+
135
+ - Consultez la [documentation React](../react/README.md)
136
+ - Consultez le [Quick Start](../react/QUICKSTART.md)
137
+ - Ouvrez une [issue sur GitHub](https://github.com/osfarm/itineraire-technique/issues)
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Exemple de _document.tsx pour Next.js Pages Router
3
+ * Fichier: pages/_document.tsx
4
+ */
5
+
6
+ import { Html, Head, Main, NextScript } from 'next/document';
7
+
8
+ export default function Document() {
9
+ return (
10
+ <Html lang="fr">
11
+ <Head>
12
+ {/* ECharts - Bibliothèque de graphiques */}
13
+ <script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0/dist/echarts.js"></script>
14
+
15
+ {/* jQuery et jQuery UI */}
16
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
17
+ <script src="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/dist/jquery-ui.min.js"></script>
18
+ <link
19
+ rel="stylesheet"
20
+ href="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/themes/base/jquery-ui.css"
21
+ />
22
+
23
+ {/* Underscore.js */}
24
+ <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.7/underscore-umd-min.js"></script>
25
+
26
+ {/* Bootstrap */}
27
+ <script
28
+ src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
29
+ integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
30
+ crossOrigin="anonymous"
31
+ ></script>
32
+ <link
33
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
34
+ rel="stylesheet"
35
+ integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
36
+ crossOrigin="anonymous"
37
+ />
38
+
39
+ {/* Font Awesome */}
40
+ <link
41
+ href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
42
+ rel="stylesheet"
43
+ />
44
+
45
+ {/*
46
+ Scripts TIKA - À copier dans le dossier public/
47
+ Voir instructions dans react/README.md
48
+ */}
49
+ <script src="/chart-render.js"></script>
50
+ <script src="/editor-attributes.js"></script>
51
+ <script src="/editor-interventions.js"></script>
52
+ <script src="/editor-crops.js"></script>
53
+ <script src="/editor-export.js"></script>
54
+ <script src="/editor-wiki-editor.js"></script>
55
+
56
+ {/* Styles TIKA - À copier dans le dossier public/ */}
57
+ <link href="/styles-rendering.css" rel="stylesheet" />
58
+ <link href="/styles-editor.css" rel="stylesheet" />
59
+ </Head>
60
+ <body>
61
+ <Main />
62
+ <NextScript />
63
+ </body>
64
+ </Html>
65
+ );
66
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Exemple d'API route pour sauvegarder/charger des itinéraires
3
+ * Fichier: app/api/itineraire/route.ts (Next.js App Router)
4
+ * ou pages/api/itineraire.ts (Next.js Pages Router)
5
+ */
6
+
7
+ import { NextRequest, NextResponse } from 'next/server';
8
+ import { promises as fs } from 'fs';
9
+ import path from 'path';
10
+
11
+ // Dossier de stockage des itinéraires
12
+ const ITINERAIRES_DIR = path.join(process.cwd(), 'data', 'itineraires');
13
+
14
+ // Créer le dossier s'il n'existe pas
15
+ async function ensureDir() {
16
+ try {
17
+ await fs.access(ITINERAIRES_DIR);
18
+ } catch {
19
+ await fs.mkdir(ITINERAIRES_DIR, { recursive: true });
20
+ }
21
+ }
22
+
23
+ // GET - Récupérer un itinéraire
24
+ export async function GET(request: NextRequest) {
25
+ const { searchParams } = new URL(request.url);
26
+ const id = searchParams.get('id');
27
+
28
+ if (!id) {
29
+ return NextResponse.json({ error: 'ID manquant' }, { status: 400 });
30
+ }
31
+
32
+ try {
33
+ await ensureDir();
34
+ const filePath = path.join(ITINERAIRES_DIR, `${id}.json`);
35
+ const data = await fs.readFile(filePath, 'utf-8');
36
+ return NextResponse.json(JSON.parse(data));
37
+ } catch (error) {
38
+ return NextResponse.json(
39
+ { error: 'Itinéraire non trouvé' },
40
+ { status: 404 }
41
+ );
42
+ }
43
+ }
44
+
45
+ // POST - Sauvegarder un itinéraire
46
+ export async function POST(request: NextRequest) {
47
+ try {
48
+ const data = await request.json();
49
+
50
+ // Validation basique
51
+ if (!data.title || !data.options || !data.steps) {
52
+ return NextResponse.json(
53
+ { error: 'Données invalides' },
54
+ { status: 400 }
55
+ );
56
+ }
57
+
58
+ await ensureDir();
59
+
60
+ // Générer un ID si nécessaire
61
+ const id = data.id || `itk-${Date.now()}`;
62
+ const filePath = path.join(ITINERAIRES_DIR, `${id}.json`);
63
+
64
+ // Sauvegarder
65
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
66
+
67
+ return NextResponse.json({ success: true, id });
68
+ } catch (error) {
69
+ console.error('Erreur de sauvegarde:', error);
70
+ return NextResponse.json(
71
+ { error: 'Erreur de sauvegarde' },
72
+ { status: 500 }
73
+ );
74
+ }
75
+ }
76
+
77
+ // DELETE - Supprimer un itinéraire
78
+ export async function DELETE(request: NextRequest) {
79
+ const { searchParams } = new URL(request.url);
80
+ const id = searchParams.get('id');
81
+
82
+ if (!id) {
83
+ return NextResponse.json({ error: 'ID manquant' }, { status: 400 });
84
+ }
85
+
86
+ try {
87
+ const filePath = path.join(ITINERAIRES_DIR, `${id}.json`);
88
+ await fs.unlink(filePath);
89
+ return NextResponse.json({ success: true });
90
+ } catch (error) {
91
+ return NextResponse.json(
92
+ { error: 'Erreur de suppression' },
93
+ { status: 500 }
94
+ );
95
+ }
96
+ }
97
+
98
+ // Liste de tous les itinéraires (pour Pages Router)
99
+ // Fichier: pages/api/itineraires/list.ts
100
+ export async function listItineraires() {
101
+ await ensureDir();
102
+ const files = await fs.readdir(ITINERAIRES_DIR);
103
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
104
+
105
+ const itineraires = await Promise.all(
106
+ jsonFiles.map(async (file) => {
107
+ const content = await fs.readFile(
108
+ path.join(ITINERAIRES_DIR, file),
109
+ 'utf-8'
110
+ );
111
+ const data = JSON.parse(content);
112
+ return {
113
+ id: file.replace('.json', ''),
114
+ title: data.title,
115
+ stepsCount: data.steps?.length || 0,
116
+ updatedAt: (await fs.stat(path.join(ITINERAIRES_DIR, file))).mtime
117
+ };
118
+ })
119
+ );
120
+
121
+ return itineraires;
122
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Exemple d'utilisation de l'éditeur avec Next.js App Router
3
+ * Fichier: app/editor/page.tsx
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import { useItineraire, useItineraireDependencies } from '@osfarm/itineraire-technique/react';
9
+ import { TikaRenderer } from '@osfarm/itineraire-technique/react';
10
+ import { useState } from 'react';
11
+ import type { ItineraireData, Step, Intervention } from '@osfarm/itineraire-technique/react';
12
+
13
+ export default function EditorPage() {
14
+ const [isSaving, setIsSaving] = useState(false);
15
+ const [saveMessage, setSaveMessage] = useState<string | null>(null);
16
+
17
+ // Vérifier que les dépendances sont chargées
18
+ const { loaded: depsLoaded, error: depsError } = useItineraireDependencies();
19
+
20
+ // Données initiales
21
+ const initialData: ItineraireData = {
22
+ title: "Nouvelle rotation",
23
+ options: {
24
+ view: "horizontal",
25
+ show_transcript: true,
26
+ title_top_interventions: "Cultures principales",
27
+ title_bottom_interventions: "Couverts et CIVE",
28
+ title_steps: "Rotation",
29
+ region: "France",
30
+ show_climate_diagram: false
31
+ },
32
+ steps: []
33
+ };
34
+
35
+ const {
36
+ data,
37
+ loading,
38
+ error,
39
+ addStep,
40
+ updateStep,
41
+ deleteStep,
42
+ addIntervention,
43
+ deleteIntervention,
44
+ exportToJson,
45
+ importFromJson
46
+ } = useItineraire(initialData);
47
+
48
+ // Sauvegarder vers une API
49
+ const handleSave = async () => {
50
+ if (!data) return;
51
+
52
+ setIsSaving(true);
53
+ setSaveMessage(null);
54
+
55
+ try {
56
+ const response = await fetch('/api/itineraire', {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(data),
62
+ });
63
+
64
+ if (!response.ok) throw new Error('Erreur de sauvegarde');
65
+
66
+ setSaveMessage('Sauvegarde réussie !');
67
+ setTimeout(() => setSaveMessage(null), 3000);
68
+ } catch (err) {
69
+ setSaveMessage(`Erreur: ${err instanceof Error ? err.message : 'Erreur inconnue'}`);
70
+ } finally {
71
+ setIsSaving(false);
72
+ }
73
+ };
74
+
75
+ // Exporter en JSON
76
+ const handleExport = () => {
77
+ const json = exportToJson();
78
+ const blob = new Blob([json], { type: 'application/json' });
79
+ const url = URL.createObjectURL(blob);
80
+ const a = document.createElement('a');
81
+ a.href = url;
82
+ a.download = `itineraire-${Date.now()}.json`;
83
+ document.body.appendChild(a);
84
+ a.click();
85
+ document.body.removeChild(a);
86
+ URL.revokeObjectURL(url);
87
+ };
88
+
89
+ // Importer depuis JSON
90
+ const handleImport = () => {
91
+ const input = document.createElement('input');
92
+ input.type = 'file';
93
+ input.accept = 'application/json';
94
+ input.onchange = (e) => {
95
+ const file = (e.target as HTMLInputElement).files?.[0];
96
+ if (file) {
97
+ const reader = new FileReader();
98
+ reader.onload = (e) => {
99
+ const content = e.target?.result as string;
100
+ try {
101
+ importFromJson(content);
102
+ setSaveMessage('Import réussi !');
103
+ setTimeout(() => setSaveMessage(null), 3000);
104
+ } catch (err) {
105
+ setSaveMessage(`Erreur d'import: ${err instanceof Error ? err.message : 'Fichier invalide'}`);
106
+ }
107
+ };
108
+ reader.readAsText(file);
109
+ }
110
+ };
111
+ input.click();
112
+ };
113
+
114
+ // Ajouter une nouvelle étape
115
+ const handleAddStep = () => {
116
+ const now = new Date();
117
+ const endDate = new Date(now);
118
+ endDate.setMonth(endDate.getMonth() + 3);
119
+
120
+ const newStep: Step = {
121
+ id: crypto.randomUUID(),
122
+ name: `Nouvelle culture ${data?.steps.length ? data.steps.length + 1 : 1}`,
123
+ startDate: now.toISOString(),
124
+ endDate: endDate.toISOString(),
125
+ color: `#${Math.floor(Math.random()*16777215).toString(16)}`,
126
+ description: '',
127
+ interventions: []
128
+ };
129
+
130
+ addStep(newStep);
131
+ };
132
+
133
+ // Ajouter une intervention à une étape
134
+ const handleAddIntervention = (stepId: string) => {
135
+ const newIntervention: Intervention = {
136
+ id: crypto.randomUUID(),
137
+ day: '0',
138
+ name: 'Nouvelle intervention',
139
+ type: 'intervention_top',
140
+ description: ''
141
+ };
142
+
143
+ addIntervention(stepId, newIntervention);
144
+ };
145
+
146
+ if (!depsLoaded) {
147
+ return (
148
+ <div className="container py-5 text-center">
149
+ <div className="spinner-border" role="status">
150
+ <span className="visually-hidden">Chargement des dépendances...</span>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ if (depsError) {
157
+ return (
158
+ <div className="container py-5">
159
+ <div className="alert alert-danger">
160
+ Erreur de chargement: {depsError.message}
161
+ <br />
162
+ <small>Assurez-vous que les scripts sont bien chargés dans _document.tsx</small>
163
+ </div>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ if (loading) {
169
+ return (
170
+ <div className="container py-5 text-center">
171
+ <div className="spinner-border" role="status">
172
+ <span className="visually-hidden">Chargement...</span>
173
+ </div>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ if (error) {
179
+ return (
180
+ <div className="container py-5">
181
+ <div className="alert alert-danger">
182
+ Erreur: {error.message}
183
+ </div>
184
+ </div>
185
+ );
186
+ }
187
+
188
+ return (
189
+ <div className="container-fluid py-4">
190
+ {/* En-tête avec boutons d'action */}
191
+ <div className="row mb-4">
192
+ <div className="col-12">
193
+ <div className="d-flex justify-content-between align-items-center">
194
+ <h1>Éditeur d'Itinéraire Technique</h1>
195
+
196
+ <div className="btn-group" role="group">
197
+ <button
198
+ className="btn btn-primary"
199
+ onClick={handleSave}
200
+ disabled={isSaving}
201
+ >
202
+ <i className="fa fa-save"></i>
203
+ {isSaving ? ' Sauvegarde...' : ' Sauvegarder'}
204
+ </button>
205
+ <button
206
+ className="btn btn-outline-secondary"
207
+ onClick={handleExport}
208
+ >
209
+ <i className="fa fa-download"></i> Exporter JSON
210
+ </button>
211
+ <button
212
+ className="btn btn-outline-secondary"
213
+ onClick={handleImport}
214
+ >
215
+ <i className="fa fa-upload"></i> Importer JSON
216
+ </button>
217
+ </div>
218
+ </div>
219
+
220
+ {saveMessage && (
221
+ <div className={`alert mt-3 ${saveMessage.includes('Erreur') ? 'alert-danger' : 'alert-success'}`}>
222
+ {saveMessage}
223
+ </div>
224
+ )}
225
+ </div>
226
+ </div>
227
+
228
+ {/* Zone d'édition */}
229
+ <div className="row">
230
+ <div className="col-lg-3">
231
+ <div className="card">
232
+ <div className="card-header d-flex justify-content-between align-items-center">
233
+ <h5 className="mb-0">Étapes</h5>
234
+ <button
235
+ className="btn btn-sm btn-success"
236
+ onClick={handleAddStep}
237
+ >
238
+ <i className="fa fa-plus"></i>
239
+ </button>
240
+ </div>
241
+ <div className="card-body">
242
+ {data?.steps.length === 0 ? (
243
+ <p className="text-muted text-center">Aucune étape</p>
244
+ ) : (
245
+ <div className="list-group">
246
+ {data?.steps.map((step) => (
247
+ <div key={step.id} className="list-group-item">
248
+ <div className="d-flex justify-content-between align-items-center mb-2">
249
+ <strong>{step.name}</strong>
250
+ <div className="btn-group btn-group-sm">
251
+ <button
252
+ className="btn btn-outline-primary"
253
+ onClick={() => handleAddIntervention(step.id)}
254
+ title="Ajouter intervention"
255
+ >
256
+ <i className="fa fa-plus"></i>
257
+ </button>
258
+ <button
259
+ className="btn btn-outline-danger"
260
+ onClick={() => deleteStep(step.id)}
261
+ title="Supprimer"
262
+ >
263
+ <i className="fa fa-trash"></i>
264
+ </button>
265
+ </div>
266
+ </div>
267
+ <small className="text-muted">
268
+ {new Date(step.startDate).toLocaleDateString()} -
269
+ {new Date(step.endDate).toLocaleDateString()}
270
+ </small>
271
+ {step.interventions && step.interventions.length > 0 && (
272
+ <div className="mt-2">
273
+ <small>{step.interventions.length} intervention(s)</small>
274
+ </div>
275
+ )}
276
+ </div>
277
+ ))}
278
+ </div>
279
+ )}
280
+ </div>
281
+ </div>
282
+ </div>
283
+
284
+ <div className="col-lg-9">
285
+ <div className="card">
286
+ <div className="card-header">
287
+ <h5 className="mb-0">Aperçu</h5>
288
+ </div>
289
+ <div className="card-body">
290
+ {data && data.steps.length > 0 ? (
291
+ <TikaRenderer data={data} width="100%" height="auto" />
292
+ ) : (
293
+ <div className="text-center text-muted py-5">
294
+ <i className="fa fa-info-circle fa-3x mb-3"></i>
295
+ <p>Ajoutez des étapes pour voir l'aperçu</p>
296
+ </div>
297
+ )}
298
+ </div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ );
304
+ }