@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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * TikaEditor - Composant React pour éditer un itinéraire technique
3
+ *
4
+ * @example
5
+ * import TikaEditor from '@osfarm/itineraire-technique/react/TikaEditor';
6
+ *
7
+ * function MyComponent() {
8
+ * const [data, setData] = useState(initialData);
9
+ *
10
+ * const handleSave = (newData) => {
11
+ * console.log('Data saved:', newData);
12
+ * setData(newData);
13
+ * };
14
+ *
15
+ * return <TikaEditor initialData={data} onSave={handleSave} />;
16
+ * }
17
+ */
18
+
19
+ import React, { useEffect, useRef, useState } from 'react';
20
+
21
+ const TikaEditor = ({
22
+ initialData = null,
23
+ onSave = null,
24
+ onExport = null,
25
+ className = '',
26
+ showWikiButtons = false,
27
+ enableAutoSave = false,
28
+ autoSaveInterval = 5000
29
+ }) => {
30
+ const containerRef = useRef(null);
31
+ const editorInitialized = useRef(false);
32
+ const autoSaveTimer = useRef(null);
33
+ const [isLoading, setIsLoading] = useState(true);
34
+ const [currentData, setCurrentData] = useState(initialData);
35
+
36
+ // Fonction pour récupérer les données actuelles de l'éditeur
37
+ const getCurrentData = () => {
38
+ if (typeof window === 'undefined' || !window.rotation_data) return null;
39
+ return JSON.parse(JSON.stringify(window.rotation_data));
40
+ };
41
+
42
+ // Fonction pour sauvegarder
43
+ const handleSave = () => {
44
+ const data = getCurrentData();
45
+ if (data && onSave) {
46
+ onSave(data);
47
+ setCurrentData(data);
48
+ }
49
+ };
50
+
51
+ // Fonction pour exporter
52
+ const handleExport = () => {
53
+ const data = getCurrentData();
54
+ if (data && onExport) {
55
+ onExport(data);
56
+ } else if (data && window.exportToJsonFile) {
57
+ // Utiliser la fonction d'export native si pas de callback personnalisé
58
+ window.exportToJsonFile(data);
59
+ }
60
+ };
61
+
62
+ // Auto-save
63
+ useEffect(() => {
64
+ if (enableAutoSave && onSave) {
65
+ autoSaveTimer.current = setInterval(() => {
66
+ handleSave();
67
+ }, autoSaveInterval);
68
+
69
+ return () => {
70
+ if (autoSaveTimer.current) {
71
+ clearInterval(autoSaveTimer.current);
72
+ }
73
+ };
74
+ }
75
+ }, [enableAutoSave, autoSaveInterval, onSave]);
76
+
77
+ useEffect(() => {
78
+ if (typeof window === 'undefined') return;
79
+
80
+ // Vérifier les dépendances
81
+ const checkDependencies = () => {
82
+ const missing = [];
83
+ if (!window.$) missing.push('jQuery');
84
+ if (!window.echarts) missing.push('ECharts');
85
+ if (!window._) missing.push('Underscore');
86
+ if (!window.RotationRenderer) missing.push('chart-render.js');
87
+
88
+ // Vérifier les fonctions de l'éditeur
89
+ if (!window.refreshAttributesTable) missing.push('editor-attributes.js');
90
+ if (!window.refreshStepsButtonList) missing.push('editor-crops.js');
91
+ if (!window.exportToJsonFile) missing.push('editor-export.js');
92
+ if (!window.refreshInterventionsList) missing.push('editor-interventions.js');
93
+
94
+ return missing;
95
+ };
96
+
97
+ const missingDeps = checkDependencies();
98
+ if (missingDeps.length > 0) {
99
+ console.error('Missing dependencies for TikaEditor:', missingDeps.join(', '));
100
+ setIsLoading(false);
101
+ return;
102
+ }
103
+
104
+ // Initialiser l'éditeur seulement une fois
105
+ if (!editorInitialized.current && containerRef.current) {
106
+ editorInitialized.current = true;
107
+
108
+ // Initialiser la structure globale de données si nécessaire
109
+ if (!window.rotation_data) {
110
+ window.rotation_data = initialData || {
111
+ title: "Nouvel itinéraire",
112
+ options: {
113
+ view: "horizontal",
114
+ show_transcript: true,
115
+ title_top_interventions: "Cultures principales",
116
+ title_bottom_interventions: "Couverts et CIVE",
117
+ title_steps: "Rotation",
118
+ region: "France",
119
+ show_climate_diagram: false
120
+ },
121
+ steps: []
122
+ };
123
+ } else if (initialData) {
124
+ window.rotation_data = initialData;
125
+ }
126
+
127
+ // Appeler les fonctions d'initialisation de l'éditeur
128
+ try {
129
+ if (window.refreshAttributesTable) window.refreshAttributesTable();
130
+ if (window.refreshStepsButtonList) window.refreshStepsButtonList();
131
+ if (window.refreshInterventionsList) window.refreshInterventionsList();
132
+
133
+ // Rendre le graphique initial
134
+ if (window.renderChart) {
135
+ window.renderChart();
136
+ }
137
+
138
+ setIsLoading(false);
139
+ } catch (error) {
140
+ console.error('Error initializing editor:', error);
141
+ setIsLoading(false);
142
+ }
143
+ }
144
+
145
+ // Exposer les fonctions de save/export via window pour que le HTML interne puisse les appeler
146
+ window.reactEditorSave = handleSave;
147
+ window.reactEditorExport = handleExport;
148
+
149
+ }, [initialData]);
150
+
151
+ // Mettre à jour les données si initialData change
152
+ useEffect(() => {
153
+ if (initialData && window.rotation_data) {
154
+ window.rotation_data = initialData;
155
+ if (window.refreshAttributesTable) window.refreshAttributesTable();
156
+ if (window.refreshStepsButtonList) window.refreshStepsButtonList();
157
+ if (window.refreshInterventionsList) window.refreshInterventionsList();
158
+ if (window.renderChart) window.renderChart();
159
+ }
160
+ }, [initialData]);
161
+
162
+ if (isLoading) {
163
+ return (
164
+ <div className={`itineraire-editor-loading ${className}`}>
165
+ <div className="spinner-border" role="status">
166
+ <span className="visually-hidden">Chargement...</span>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ return (
173
+ <div ref={containerRef} className={`itineraire-editor ${className}`}>
174
+ {/* Boutons de contrôle React */}
175
+ <div className="react-editor-controls mb-3">
176
+ <div className="btn-group" role="group">
177
+ {onSave && (
178
+ <button
179
+ type="button"
180
+ className="btn btn-primary"
181
+ onClick={handleSave}
182
+ >
183
+ <i className="fa fa-save"></i> Enregistrer
184
+ </button>
185
+ )}
186
+ {onExport && (
187
+ <button
188
+ type="button"
189
+ className="btn btn-outline-primary"
190
+ onClick={handleExport}
191
+ >
192
+ <i className="fa fa-download"></i> Exporter JSON
193
+ </button>
194
+ )}
195
+ </div>
196
+ </div>
197
+
198
+ {/* Le contenu HTML de l'éditeur sera injecté ici via un portail ou iframe */}
199
+ {/* Pour l'instant, on délègue au HTML existant */}
200
+ <div id="editor-content-container">
201
+ {/* L'éditeur HTML existant sera chargé ici */}
202
+ <iframe
203
+ src="/editor.html"
204
+ style={{ width: '100%', height: '800px', border: 'none' }}
205
+ title="Éditeur d'itinéraire technique"
206
+ />
207
+ </div>
208
+ </div>
209
+ );
210
+ };
211
+
212
+ export default TikaEditor;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * TikaRenderer - Composant React pour visualiser un itinéraire technique
3
+ *
4
+ * @example
5
+ * import TikaRenderer from '@osfarm/itineraire-technique/react/TikaRenderer';
6
+ *
7
+ * function MyComponent() {
8
+ * const data = { ... }; // Données JSON de l'itinéraire
9
+ * return <TikaRenderer data={data} />;
10
+ * }
11
+ */
12
+
13
+ import React, { useEffect, useRef } from 'react';
14
+
15
+ const TikaRenderer = ({
16
+ data,
17
+ width = '100%',
18
+ height = 'auto',
19
+ className = '',
20
+ onItemClick = null,
21
+ onItemHover = null
22
+ }) => {
23
+ const containerRef = useRef(null);
24
+ const rendererRef = useRef(null);
25
+ const divId = useRef(`itk-${Math.random().toString(36).substr(2, 9)}`);
26
+
27
+ useEffect(() => {
28
+ // Vérifier que les dépendances sont chargées
29
+ if (typeof window === 'undefined') return;
30
+
31
+ if (!window.RotationRenderer) {
32
+ console.error('RotationRenderer not loaded. Make sure chart-render.js is included.');
33
+ return;
34
+ }
35
+
36
+ if (!window.echarts) {
37
+ console.error('ECharts not loaded. Make sure ECharts is included.');
38
+ return;
39
+ }
40
+
41
+ if (!window.$) {
42
+ console.error('jQuery not loaded. Make sure jQuery is included.');
43
+ return;
44
+ }
45
+
46
+ // Créer le conteneur si nécessaire
47
+ if (containerRef.current && !containerRef.current.querySelector('.mainITKContainer')) {
48
+ // Initialiser le renderer
49
+ try {
50
+ rendererRef.current = new window.RotationRenderer(divId.current, data);
51
+ rendererRef.current.render();
52
+
53
+ // Attacher les événements personnalisés si fournis
54
+ if (onItemClick) {
55
+ containerRef.current.addEventListener('click', (e) => {
56
+ const item = e.target.closest('.rotation_item');
57
+ if (item) {
58
+ const itemId = item.dataset.id || item.id;
59
+ onItemClick(itemId, e);
60
+ }
61
+ });
62
+ }
63
+
64
+ if (onItemHover) {
65
+ containerRef.current.addEventListener('mouseover', (e) => {
66
+ const item = e.target.closest('.rotation_item');
67
+ if (item) {
68
+ const itemId = item.dataset.id || item.id;
69
+ onItemHover(itemId, e);
70
+ }
71
+ });
72
+ }
73
+ } catch (error) {
74
+ console.error('Error initializing RotationRenderer:', error);
75
+ }
76
+ }
77
+
78
+ // Cleanup
79
+ return () => {
80
+ if (rendererRef.current && rendererRef.current.chart) {
81
+ try {
82
+ rendererRef.current.chart.dispose();
83
+ } catch (error) {
84
+ console.error('Error disposing chart:', error);
85
+ }
86
+ }
87
+ };
88
+ }, [data, onItemClick, onItemHover]);
89
+
90
+ // Réagir aux changements de données
91
+ useEffect(() => {
92
+ if (rendererRef.current && data) {
93
+ try {
94
+ // Recréer le renderer avec les nouvelles données
95
+ if (rendererRef.current.chart) {
96
+ rendererRef.current.chart.dispose();
97
+ }
98
+ rendererRef.current = new window.RotationRenderer(divId.current, data);
99
+ rendererRef.current.render();
100
+ } catch (error) {
101
+ console.error('Error updating chart data:', error);
102
+ }
103
+ }
104
+ }, [data]);
105
+
106
+ return (
107
+ <div
108
+ ref={containerRef}
109
+ id={divId.current}
110
+ className={`itineraire-renderer ${className}`}
111
+ style={{ width, height }}
112
+ />
113
+ );
114
+ };
115
+
116
+ export default TikaRenderer;
package/react/hooks.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Hook React personnalisé pour gérer les données d'itinéraire technique
3
+ */
4
+
5
+ import { useState, useCallback, useEffect } from 'react';
6
+ import type { ItineraireData, Step, Intervention } from './types';
7
+
8
+ export interface UseItineraireResult {
9
+ data: ItineraireData | null;
10
+ loading: boolean;
11
+ error: Error | null;
12
+ updateData: (newData: ItineraireData) => void;
13
+ addStep: (step: Step) => void;
14
+ updateStep: (stepId: string, updates: Partial<Step>) => void;
15
+ deleteStep: (stepId: string) => void;
16
+ addIntervention: (stepId: string, intervention: Intervention) => void;
17
+ updateIntervention: (stepId: string, interventionId: string, updates: Partial<Intervention>) => void;
18
+ deleteIntervention: (stepId: string, interventionId: string) => void;
19
+ exportToJson: () => string;
20
+ importFromJson: (jsonString: string) => void;
21
+ reset: () => void;
22
+ }
23
+
24
+ /**
25
+ * Hook pour gérer les données d'itinéraire technique
26
+ * @param initialData Données initiales
27
+ * @param onUpdate Callback appelé à chaque modification
28
+ */
29
+ export const useItineraire = (
30
+ initialData: ItineraireData | null = null,
31
+ onUpdate?: (data: ItineraireData) => void
32
+ ): UseItineraireResult => {
33
+ const [data, setData] = useState<ItineraireData | null>(initialData);
34
+ const [loading, setLoading] = useState(false);
35
+ const [error, setError] = useState<Error | null>(null);
36
+
37
+ // Appeler onUpdate quand les données changent
38
+ useEffect(() => {
39
+ if (data && onUpdate) {
40
+ onUpdate(data);
41
+ }
42
+ }, [data, onUpdate]);
43
+
44
+ const updateData = useCallback((newData: ItineraireData) => {
45
+ setData(newData);
46
+ setError(null);
47
+ }, []);
48
+
49
+ const addStep = useCallback((step: Step) => {
50
+ setData((prevData) => {
51
+ if (!prevData) return prevData;
52
+ return {
53
+ ...prevData,
54
+ steps: [...prevData.steps, step]
55
+ };
56
+ });
57
+ }, []);
58
+
59
+ const updateStep = useCallback((stepId: string, updates: Partial<Step>) => {
60
+ setData((prevData) => {
61
+ if (!prevData) return prevData;
62
+ return {
63
+ ...prevData,
64
+ steps: prevData.steps.map((step) =>
65
+ step.id === stepId ? { ...step, ...updates } : step
66
+ )
67
+ };
68
+ });
69
+ }, []);
70
+
71
+ const deleteStep = useCallback((stepId: string) => {
72
+ setData((prevData) => {
73
+ if (!prevData) return prevData;
74
+ return {
75
+ ...prevData,
76
+ steps: prevData.steps.filter((step) => step.id !== stepId)
77
+ };
78
+ });
79
+ }, []);
80
+
81
+ const addIntervention = useCallback((stepId: string, intervention: Intervention) => {
82
+ setData((prevData) => {
83
+ if (!prevData) return prevData;
84
+ return {
85
+ ...prevData,
86
+ steps: prevData.steps.map((step) =>
87
+ step.id === stepId
88
+ ? {
89
+ ...step,
90
+ interventions: [...(step.interventions || []), intervention]
91
+ }
92
+ : step
93
+ )
94
+ };
95
+ });
96
+ }, []);
97
+
98
+ const updateIntervention = useCallback(
99
+ (stepId: string, interventionId: string, updates: Partial<Intervention>) => {
100
+ setData((prevData) => {
101
+ if (!prevData) return prevData;
102
+ return {
103
+ ...prevData,
104
+ steps: prevData.steps.map((step) =>
105
+ step.id === stepId
106
+ ? {
107
+ ...step,
108
+ interventions: (step.interventions || []).map((intervention) =>
109
+ intervention.id === interventionId
110
+ ? { ...intervention, ...updates }
111
+ : intervention
112
+ )
113
+ }
114
+ : step
115
+ )
116
+ };
117
+ });
118
+ },
119
+ []
120
+ );
121
+
122
+ const deleteIntervention = useCallback((stepId: string, interventionId: string) => {
123
+ setData((prevData) => {
124
+ if (!prevData) return prevData;
125
+ return {
126
+ ...prevData,
127
+ steps: prevData.steps.map((step) =>
128
+ step.id === stepId
129
+ ? {
130
+ ...step,
131
+ interventions: (step.interventions || []).filter(
132
+ (intervention) => intervention.id !== interventionId
133
+ )
134
+ }
135
+ : step
136
+ )
137
+ };
138
+ });
139
+ }, []);
140
+
141
+ const exportToJson = useCallback(() => {
142
+ if (!data) return '{}';
143
+ return JSON.stringify(data, null, 2);
144
+ }, [data]);
145
+
146
+ const importFromJson = useCallback((jsonString: string) => {
147
+ try {
148
+ setLoading(true);
149
+ const parsed = JSON.parse(jsonString);
150
+ setData(parsed);
151
+ setError(null);
152
+ } catch (err) {
153
+ setError(err instanceof Error ? err : new Error('Invalid JSON'));
154
+ } finally {
155
+ setLoading(false);
156
+ }
157
+ }, []);
158
+
159
+ const reset = useCallback(() => {
160
+ setData(initialData);
161
+ setError(null);
162
+ }, [initialData]);
163
+
164
+ return {
165
+ data,
166
+ loading,
167
+ error,
168
+ updateData,
169
+ addStep,
170
+ updateStep,
171
+ deleteStep,
172
+ addIntervention,
173
+ updateIntervention,
174
+ deleteIntervention,
175
+ exportToJson,
176
+ importFromJson,
177
+ reset
178
+ };
179
+ };
180
+
181
+ /**
182
+ * Hook pour charger les dépendances nécessaires (ECharts, jQuery, etc.)
183
+ */
184
+ export const useItineraireDependencies = () => {
185
+ const [loaded, setLoaded] = useState(false);
186
+ const [error, setError] = useState<Error | null>(null);
187
+
188
+ useEffect(() => {
189
+ if (typeof window === 'undefined') return;
190
+
191
+ const checkDependencies = () => {
192
+ const hasEcharts = !!window.echarts;
193
+ const hasJQuery = !!window.$;
194
+ const hasUnderscore = !!window._;
195
+ const hasRotationRenderer = !!window.RotationRenderer;
196
+
197
+ return hasEcharts && hasJQuery && hasUnderscore && hasRotationRenderer;
198
+ };
199
+
200
+ if (checkDependencies()) {
201
+ setLoaded(true);
202
+ } else {
203
+ // Attendre un peu que les scripts se chargent
204
+ const timeout = setTimeout(() => {
205
+ if (checkDependencies()) {
206
+ setLoaded(true);
207
+ } else {
208
+ setError(new Error('Required dependencies not loaded'));
209
+ }
210
+ }, 1000);
211
+
212
+ return () => clearTimeout(timeout);
213
+ }
214
+ }, []);
215
+
216
+ return { loaded, error };
217
+ };
package/react/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Point d'entrée principal pour les composants React
3
+ */
4
+
5
+ export { default as TikaRenderer } from './TikaRenderer';
6
+ export { default as TikaEditor } from './TikaEditor';
7
+ export { useItineraire, useItineraireDependencies } from './hooks';
8
+ export type {
9
+ ItineraireData,
10
+ ItineraireOptions,
11
+ Step,
12
+ Intervention,
13
+ ClimateData,
14
+ RotationData,
15
+ TimelineData,
16
+ TikaRendererProps,
17
+ TikaEditorProps,
18
+ IRotationRenderer
19
+ } from './types';